Panda3D

Loading...
Searching...
No Matches
daeMaterials.cxx
Go to the documentation of this file.
1/**
2 * PANDA 3D SOFTWARE
3 * Copyright (c) Carnegie Mellon University. All rights reserved.
4 *
5 * All use of this software is subject to the terms of the revised BSD
6 * license. You should have received a copy of this license along
7 * with this source code in a file named "LICENSE."
8 *
9 * @file daeMaterials.cxx
10 * @author rdb
11 * @date 2008-10-03
12 */
13
14#include "daeMaterials.h"
15#include "config_daeegg.h"
16#include "fcollada_utils.h"
17
18#include <FCDocument/FCDocument.h>
19#include <FCDocument/FCDMaterial.h>
20#include <FCDocument/FCDEffect.h>
21#include <FCDocument/FCDTexture.h>
22#include <FCDocument/FCDEffectParameterSampler.h>
23#include <FCDocument/FCDImage.h>
24
25#include "filename.h"
26#include "string_utils.h"
27
28 using std::endl;
29 using std::string;
30
31 TypeHandle DaeMaterials::_type_handle;
32
33// luminance function, based on the ISOCIE color standards see ITU-R
34// Recommendation BT.709-4
35#define luminance(c) ((c[0] * 0.212671 + c[1] * 0.715160 + c[2] * 0.072169))
36
37/**
38 *
39 */
40DaeMaterials::
41DaeMaterials(const FCDGeometryInstance* geometry_instance) {
42 for (size_t mi = 0; mi < geometry_instance->GetMaterialInstanceCount(); ++mi) {
43 add_material_instance(geometry_instance->GetMaterialInstance(mi));
44 }
45}
46
47/**
48 * Adds a material instance. Normally automatically done by constructor.
49 */
50 void DaeMaterials::add_material_instance(const FCDMaterialInstance* instance) {
51 nassertv(instance != nullptr);
52 const string semantic (FROM_FSTRING(instance->GetSemantic()));
53 if (_materials.count(semantic) > 0) {
54 daeegg_cat.warning() << "Ignoring duplicate material with semantic " << semantic << endl;
55 return;
56 }
57 _materials[semantic] = new DaeMaterial();
58
59 // Load in the uvsets
60 for (size_t vib = 0; vib < instance->GetVertexInputBindingCount(); ++vib) {
61 const FCDMaterialInstanceBindVertexInput* mivib = instance->GetVertexInputBinding(vib);
62 assert(mivib != nullptr);
63 PT(DaeVertexInputBinding) bvi = new DaeVertexInputBinding();
64 bvi->_input_set = mivib->inputSet;
65#if FCOLLADA_VERSION >= 0x00030005
66 bvi->_input_semantic = mivib->GetInputSemantic();
67 bvi->_semantic = *mivib->semantic;
68#else
69 bvi->_input_semantic = mivib->inputSemantic;
70 bvi->_semantic = FROM_FSTRING(mivib->semantic);
71#endif
72 _materials[semantic]->_uvsets.push_back(bvi);
73 }
74
75 // Handle the material stuff
76 daeegg_cat.spam() << "Trying to process material with semantic " << semantic << endl;
77 PT_EggMaterial egg_material = new EggMaterial(semantic);
78 pvector<PT_EggTexture> egg_textures;
79 const FCDEffect* effect = instance->GetMaterial()->GetEffect();
80 if (effect == nullptr) {
81 daeegg_cat.debug() << "Ignoring material (semantic: " << semantic << ") without assigned effect" << endl;
82 } else {
83 // Grab the common profile effect
84 const FCDEffectStandard* effect_common = (FCDEffectStandard *)effect->FindProfile(FUDaeProfileType::COMMON);
85 if (effect_common == nullptr) {
86 daeegg_cat.info() << "Ignoring effect referenced by material with semantic " << semantic
87 << " because it has no common profile" << endl;
88 } else {
89 daeegg_cat.spam() << "Processing effect, material semantic is " << semantic << endl;
90 // Set the material parameters
91 egg_material->set_amb(TO_COLOR(effect_common->GetAmbientColor()));
92 // We already process transparency using blend modes LVecBase4 diffuse =
93 // TO_COLOR(effect_common->GetDiffuseColor());
94 // diffuse.set_w(diffuse.get_w() * (1.0f -
95 // effect_common->GetOpacity())); egg_material->set_diff(diffuse);
96 egg_material->set_diff(TO_COLOR(effect_common->GetDiffuseColor()));
97 egg_material->set_emit(TO_COLOR(effect_common->GetEmissionColor()) * effect_common->GetEmissionFactor());
98 egg_material->set_shininess(effect_common->GetShininess());
99 egg_material->set_spec(TO_COLOR(effect_common->GetSpecularColor()));
100 // Now try to load in the textures
101 process_texture_bucket(semantic, effect_common, FUDaeTextureChannel::DIFFUSE, EggTexture::ET_modulate);
102 process_texture_bucket(semantic, effect_common, FUDaeTextureChannel::BUMP, EggTexture::ET_normal);
103 process_texture_bucket(semantic, effect_common, FUDaeTextureChannel::SPECULAR, EggTexture::ET_modulate_gloss);
104 process_texture_bucket(semantic, effect_common, FUDaeTextureChannel::SPECULAR_LEVEL, EggTexture::ET_gloss);
105 process_texture_bucket(semantic, effect_common, FUDaeTextureChannel::TRANSPARENT, EggTexture::ET_unspecified, EggTexture::F_alpha);
106 process_texture_bucket(semantic, effect_common, FUDaeTextureChannel::EMISSION, EggTexture::ET_add);
107#if FCOLLADA_VERSION < 0x00030005
108 process_texture_bucket(semantic, effect_common, FUDaeTextureChannel::OPACITY, EggTexture::ET_unspecified, EggTexture::F_alpha);
109#endif
110 // Now, calculate the color blend stuff.
111 _materials[semantic]->_blend = convert_blend(effect_common->GetTransparencyMode(),
112 TO_COLOR(effect_common->GetTranslucencyColor()),
113 effect_common->GetTranslucencyFactor());
114 }
115 // Find an <extra> tag to support some extra stuff from extensions
116 process_extra(semantic, effect->GetExtra());
117 }
118 daeegg_cat.spam() << "Found " << egg_textures.size() << " textures in material" << endl;
119 _materials[semantic]->_egg_material = egg_material;
120}
121
122/**
123 * Processes the given texture bucket and gives the textures in it the given
124 * envtype and format.
125 */
126 void DaeMaterials::
127process_texture_bucket(const string semantic, const FCDEffectStandard* effect_common, FUDaeTextureChannel::Channel bucket, EggTexture::EnvType envtype, EggTexture::Format format) {
128 for (size_t tx = 0; tx < effect_common->GetTextureCount(bucket); ++tx) {
129 const FCDImage* image = effect_common->GetTexture(bucket, tx)->GetImage();
130 if (image == nullptr) {
131 daeegg_cat.warning() << "Texture references a nonexisting image!" << endl;
132 } else {
133 const FCDEffectParameterSampler* sampler = effect_common->GetTexture(bucket, tx)->GetSampler();
134 // FCollada only supplies absolute paths. We need to grab the document
135 // location ourselves and make the image path absolute.
136 Filename texpath;
137 if (image->GetDocument()) {
138 Filename docpath = Filename::from_os_specific(FROM_FSTRING(image->GetDocument()->GetFileUrl()));
139 docpath.make_canonical();
140 texpath = Filename::from_os_specific(FROM_FSTRING(image->GetFilename()));
141 texpath.make_canonical();
142 texpath.make_relative_to(docpath.get_dirname(), true);
143 daeegg_cat.debug() << "Found texture with path " << texpath << endl;
144 } else {
145 // Never mind.
146 texpath = Filename::from_os_specific(FROM_FSTRING(image->GetFilename()));
147 }
148 PT_EggTexture egg_texture = new EggTexture(FROM_FSTRING(image->GetDaeId()), texpath.to_os_generic());
149 // Find a set of UV coordinates
150 const FCDEffectParameterInt* uvset = effect_common->GetTexture(bucket, tx)->GetSet();
151 if (uvset != nullptr) {
152 daeegg_cat.debug() << "Texture has uv name '" << FROM_FSTRING(uvset->GetSemantic()) << "'\n";
153 string uvset_semantic (FROM_FSTRING(uvset->GetSemantic()));
154
155 // Only set the UV name if this UV set actually exists.
156 for (size_t i = 0; i < _materials[semantic]->_uvsets.size(); ++i) {
157 if (_materials[semantic]->_uvsets[i]->_semantic == uvset_semantic) {
158 egg_texture->set_uv_name(uvset_semantic);
159 break;
160 }
161 }
162 }
163 // Apply sampler stuff
164 if (sampler != nullptr) {
165 egg_texture->set_texture_type(convert_texture_type(sampler->GetSamplerType()));
166 egg_texture->set_wrap_u(convert_wrap_mode(sampler->GetWrapS()));
167 if (sampler->GetSamplerType() != FCDEffectParameterSampler::SAMPLER1D) {
168 egg_texture->set_wrap_v(convert_wrap_mode(sampler->GetWrapT()));
169 }
170 if (sampler->GetSamplerType() == FCDEffectParameterSampler::SAMPLER3D) {
171 egg_texture->set_wrap_w(convert_wrap_mode(sampler->GetWrapP()));
172 }
173 egg_texture->set_minfilter(convert_filter_type(sampler->GetMinFilter()));
174 egg_texture->set_magfilter(convert_filter_type(sampler->GetMagFilter()));
175 if (envtype != EggTexture::ET_unspecified) {
176 egg_texture->set_env_type(envtype);
177 }
178 if (format != EggTexture::F_unspecified) {
179 egg_texture->set_format(format);
180 }
181 }
182 _materials[semantic]->_egg_textures.push_back(egg_texture);
183 }
184 }
185}
186
187/**
188 * Processes the extra data in the given <extra> tag. If the given element is
189 * NULL, it just silently returns.
190 */
191 void DaeMaterials::
192process_extra(const string semantic, const FCDExtra* extra) {
193 if (extra == nullptr) return;
194 const FCDEType* etype = extra->GetDefaultType();
195 if (etype == nullptr) return;
196 for (size_t et = 0; et < etype->GetTechniqueCount(); ++et) {
197 const FCDENode* enode = ((const FCDENode*)(etype->GetTechnique(et)))->FindChildNode("double_sided");
198 if (enode != nullptr) {
199 string content = trim(enode->GetContent());
200 if (content == "1" || content == "true") {
201 _materials[semantic]->_double_sided = true;
202 } else if (content == "0" || content == "false") {
203 _materials[semantic]->_double_sided = false;
204 } else {
205 daeegg_cat.warning() << "Expected <double_sided> tag to be either 1 or 0, found '" << content << "' instead" << endl;
206 }
207 }
208 }
209}
210
211/**
212 * Applies the stuff to the given EggPrimitive.
213 */
214 void DaeMaterials::
215 apply_to_primitive(const string semantic, const PT(EggPrimitive) to) {
216 if (_materials.count(semantic) > 0) {
217 to->set_material(_materials[semantic]->_egg_material);
218 for (pvector<PT_EggTexture>::iterator it = _materials[semantic]->_egg_textures.begin(); it != _materials[semantic]->_egg_textures.end(); ++it) {
219 daeegg_cat.spam() << "Applying texture " << (*it)->get_name() << " from material with semantic " << semantic << endl;
220 to->add_texture(*it);
221 }
222 to->set_bface_flag(_materials[semantic]->_double_sided);
223 }
224}
225
226/**
227 * Applies the colorblend stuff to the given EggGroup.
228 */
229 void DaeMaterials::
230 apply_to_group(const string semantic, const PT(EggGroup) to, bool invert_transparency) {
231 if (_materials.count(semantic) > 0) {
232 PT(DaeBlendSettings) blend = _materials[semantic]->_blend;
233 if (blend && blend->_enabled) {
234 to->set_blend_mode(EggGroup::BM_add);
235 to->set_blend_color(blend->_color);
236 if (invert_transparency) {
237 to->set_blend_operand_a(blend->_operand_b);
238 to->set_blend_operand_b(blend->_operand_a);
239 } else {
240 to->set_blend_operand_a(blend->_operand_a);
241 to->set_blend_operand_b(blend->_operand_b);
242 }
243 } else if (blend && invert_transparency) {
244 to->set_blend_mode(EggGroup::BM_add);
245 to->set_blend_color(blend->_color);
246 to->set_blend_operand_a(blend->_operand_b);
247 to->set_blend_operand_b(blend->_operand_a);
248 }
249 }
250}
251
252/**
253 * Returns the semantic of the uvset with the specified input set, or an empty
254 * string if the given material has no input set.
255 */
256 const string DaeMaterials::
257 get_uvset_name(const string semantic, FUDaeGeometryInput::Semantic input_semantic, int32 input_set) {
258 if (_materials.count(semantic) > 0) {
259 if (input_set == -1 && _materials[semantic]->_uvsets.size() == 1) {
260 return _materials[semantic]->_uvsets[0]->_semantic;
261 } else {
262 for (size_t i = 0; i < _materials[semantic]->_uvsets.size(); ++i) {
263 if (_materials[semantic]->_uvsets[i]->_input_set == input_set &&
264 _materials[semantic]->_uvsets[i]->_input_semantic == input_semantic) {
265 return _materials[semantic]->_uvsets[i]->_semantic;
266 }
267 }
268 // If we can't find it, let's look again, but don't care for the
269 // input_semantic this time. The reason for this is that some tools
270 // export textangents and texbinormals bound to a uvset with input
271 // semantic TEXCOORD.
272 for (size_t i = 0; i < _materials[semantic]->_uvsets.size(); ++i) {
273 if (_materials[semantic]->_uvsets[i]->_input_set == input_set) {
274 daeegg_cat.debug() << "Using uv set with non-matching input semantic " << _materials[semantic]->_uvsets[i]->_semantic << "\n";
275 return _materials[semantic]->_uvsets[i]->_semantic;
276 }
277 }
278 daeegg_cat.debug() << "No uv set binding found for input set " << input_set << "\n";
279 }
280 }
281 return "";
282}
283
284/**
285 * Converts an FCollada sampler type to the EggTexture texture type
286 * equivalent.
287 */
288EggTexture::TextureType DaeMaterials::
289 convert_texture_type(const FCDEffectParameterSampler::SamplerType orig_type) {
290 switch (orig_type) {
291 case FCDEffectParameterSampler::SAMPLER1D:
292 return EggTexture::TT_1d_texture;
293 case FCDEffectParameterSampler::SAMPLER2D:
294 return EggTexture::TT_2d_texture;
295 case FCDEffectParameterSampler::SAMPLER3D:
296 return EggTexture::TT_3d_texture;
297 case FCDEffectParameterSampler::SAMPLERCUBE:
298 return EggTexture::TT_cube_map;
299 default:
300 daeegg_cat.warning() << "Invalid sampler type found" << endl;
301 }
302 return EggTexture::TT_unspecified;
303}
304
305/**
306 * Converts an FCollada wrap mode to the EggTexture wrap mode equivalent.
307 */
308EggTexture::WrapMode DaeMaterials::
309 convert_wrap_mode(const FUDaeTextureWrapMode::WrapMode orig_mode) {
310 switch (orig_mode) {
311 case FUDaeTextureWrapMode::NONE:
312 // FIXME: this shouldnt be unspecified
313 return EggTexture::WM_unspecified;
314 case FUDaeTextureWrapMode::WRAP:
315 return EggTexture::WM_repeat;
316 case FUDaeTextureWrapMode::MIRROR:
317 return EggTexture::WM_mirror;
318 case FUDaeTextureWrapMode::CLAMP:
319 return EggTexture::WM_clamp;
320 case FUDaeTextureWrapMode::BORDER:
321 return EggTexture::WM_border_color;
322 case FUDaeTextureWrapMode::UNKNOWN:
323 return EggTexture::WM_unspecified;
324 default:
325 daeegg_cat.warning() << "Invalid wrap mode found: " << FUDaeTextureWrapMode::ToString(orig_mode) << endl;
326 }
327 return EggTexture::WM_unspecified;
328}
329
330/**
331 * Converts an FCollada filter function to the EggTexture wrap type
332 * equivalent.
333 */
334EggTexture::FilterType DaeMaterials::
335 convert_filter_type(const FUDaeTextureFilterFunction::FilterFunction orig_type) {
336 switch (orig_type) {
337 case FUDaeTextureFilterFunction::NONE:
338 // FIXME: this shouldnt be unspecified
339 return EggTexture::FT_unspecified;
340 case FUDaeTextureFilterFunction::NEAREST:
341 return EggTexture::FT_nearest;
342 case FUDaeTextureFilterFunction::LINEAR:
343 return EggTexture::FT_linear;
344 case FUDaeTextureFilterFunction::NEAREST_MIPMAP_NEAREST:
345 return EggTexture::FT_nearest_mipmap_nearest;
346 case FUDaeTextureFilterFunction::LINEAR_MIPMAP_NEAREST:
347 return EggTexture::FT_linear_mipmap_nearest;
348 case FUDaeTextureFilterFunction::NEAREST_MIPMAP_LINEAR:
349 return EggTexture::FT_nearest_mipmap_linear;
350 case FUDaeTextureFilterFunction::LINEAR_MIPMAP_LINEAR:
351 return EggTexture::FT_linear_mipmap_linear;
352 case FUDaeTextureFilterFunction::UNKNOWN:
353 return EggTexture::FT_unspecified;
354 default:
355 daeegg_cat.warning() << "Unknown filter type found: " << FUDaeTextureFilterFunction::ToString(orig_type) << endl;
356 }
357 return EggTexture::FT_unspecified;
358}
359
360/**
361 * Converts collada blend attribs to Panda's equivalents.
362 */
363PT(DaeMaterials::DaeBlendSettings) DaeMaterials::
364convert_blend(FCDEffectStandard::TransparencyMode mode, const LColor &transparent, double transparency) {
365 // Create the DaeBlendSettings and fill it with some defaults.
366 PT(DaeBlendSettings) blend = new DaeBlendSettings();
367 blend->_enabled = true;
368 blend->_color = LColor::zero();
369 blend->_operand_a = EggGroup::BO_unspecified;
370 blend->_operand_b = EggGroup::BO_unspecified;
371
372 // First fill in the color value.
373 if (mode == FCDEffectStandard::A_ONE) {// || mode == FCDEffectStandard::A_ZERO) {
374 double value = transparent[3] * transparency;
375 blend->_color = LColor(value, value, value, value);
376 } else if (mode == FCDEffectStandard::RGB_ZERO) {//|| mode == FCDEffectStandard::RGB_ONE) {
377 blend->_color = transparent * transparency;
378 blend->_color[3] = luminance(blend->_color);
379 } else {
380 daeegg_cat.error() << "Unknown opaque type found!" << endl;
381 blend->_enabled = false;
382 return blend;
383 }
384
385 // Now figure out the operands.
386 if (mode == FCDEffectStandard::RGB_ZERO) {// || mode == FCDEffectStandard::A_ZERO) {
387 blend->_operand_a = EggGroup::BO_one_minus_constant_color;
388 blend->_operand_b = EggGroup::BO_constant_color;
389 } else if (mode == FCDEffectStandard::A_ONE) {// || mode == FCDEffectStandard::RGB_ONE) {
390 blend->_operand_a = EggGroup::BO_constant_color;
391 blend->_operand_b = EggGroup::BO_one_minus_constant_color;
392 } else {
393 daeegg_cat.error() << "Unknown opaque type found!" << endl;
394 blend->_enabled = false;
395 return blend;
396 }
397
398 // See if we can optimize out the color.
399 if (blend->_operand_a == EggGroup::BO_constant_color) {
400 if (blend->_color == LColor::zero()) {
401 blend->_operand_a = EggGroup::BO_zero;
402 } else if (blend->_color == LColor(1, 1, 1, 1)) {
403 blend->_operand_a = EggGroup::BO_one;
404 }
405 }
406 if (blend->_operand_b == EggGroup::BO_constant_color) {
407 if (blend->_color == LColor::zero()) {
408 blend->_operand_b = EggGroup::BO_zero;
409 } else if (blend->_color == LColor(1, 1, 1, 1)) {
410 blend->_operand_b = EggGroup::BO_one;
411 }
412 }
413 if (blend->_operand_a == EggGroup::BO_one_minus_constant_color) {
414 if (blend->_color == LColor::zero()) {
415 blend->_operand_a = EggGroup::BO_one;
416 } else if (blend->_color == LColor(1, 1, 1, 1)) {
417 blend->_operand_a = EggGroup::BO_zero;
418 }
419 }
420 if (blend->_operand_b == EggGroup::BO_one_minus_constant_color) {
421 if (blend->_color == LColor::zero()) {
422 blend->_operand_b = EggGroup::BO_one;
423 } else if (blend->_color == LColor(1, 1, 1, 1)) {
424 blend->_operand_b = EggGroup::BO_zero;
425 }
426 }
427
428 // See if we can entirely disable the blend.
429 if (blend->_operand_a == EggGroup::BO_one && blend->_operand_b == EggGroup::BO_zero) {
430 blend->_enabled = false;
431 }
432 return blend;
433}
DaeMaterials::convert_texture_type
static EggTexture::TextureType convert_texture_type(const FCDEffectParameterSampler::SamplerType orig_type)
Converts an FCollada sampler type to the EggTexture texture type equivalent.
Definition daeMaterials.cxx:289
DaeMaterials::add_material_instance
void add_material_instance(const FCDMaterialInstance *instance)
Adds a material instance.
Definition daeMaterials.cxx:50
DaeMaterials::convert_wrap_mode
static EggTexture::WrapMode convert_wrap_mode(const FUDaeTextureWrapMode::WrapMode orig_mode)
Converts an FCollada wrap mode to the EggTexture wrap mode equivalent.
Definition daeMaterials.cxx:309
DaeMaterials::get_uvset_name
const std::string get_uvset_name(const std::string semantic, FUDaeGeometryInput::Semantic input_semantic, int32 input_set)
Returns the semantic of the uvset with the specified input set, or an empty string if the given mater...
Definition daeMaterials.cxx:257
DaeMaterials::apply_to_group
void apply_to_group(const std::string semantic, const PT(EggGroup) to, bool invert_transparency=false)
Applies the colorblend stuff to the given EggGroup.
Definition daeMaterials.cxx:230
DaeMaterials::apply_to_primitive
void apply_to_primitive(const std::string semantic, const PT(EggPrimitive) to)
Applies the stuff to the given EggPrimitive.
Definition daeMaterials.cxx:215
DaeMaterials::convert_filter_type
static EggTexture::FilterType convert_filter_type(const FUDaeTextureFilterFunction::FilterFunction orig_type)
Converts an FCollada filter function to the EggTexture wrap type equivalent.
Definition daeMaterials.cxx:335
EggGroup
The main glue of the egg hierarchy, this corresponds to the <Group>, <Instance>, and <Joint> type nod...
Definition eggGroup.h:34
EggMaterial
Definition eggMaterial.h:26
EggPrimitive
A base class for any of a number of kinds of geometry primitives: polygons, point lights,...
Definition eggPrimitive.h:49
EggPrimitive::set_bface_flag
set_bface_flag
Sets the backfacing flag of the polygon.
Definition eggPrimitive.h:116
EggPrimitive::set_material
set_material
Applies the indicated material to the primitive.
Definition eggPrimitive.h:115
EggPrimitive::add_texture
void add_texture(EggTexture *texture)
Applies the indicated texture to the primitive.
Definition eggPrimitive.I:162
Filename::make_canonical
bool make_canonical()
Converts this filename to a canonical name by replacing the directory part with the fully-qualified d...
Definition filename.cxx:1011
Filename::from_os_specific
static Filename from_os_specific(const std::string &os_specific, Type type=T_general)
This named constructor returns a Panda-style filename (that is, using forward slashes,...
Definition filename.cxx:328
Filename::to_os_generic
std::string to_os_generic() const
This is similar to to_os_specific(), but it is designed to generate a filename that can be understood...
Definition filename.cxx:1182
Filename::make_relative_to
bool make_relative_to(Filename directory, bool allow_backups=true)
Adjusts this filename, which must be a fully-specified pathname beginning with a slash,...
Definition filename.cxx:1649
Filename::get_dirname
std::string get_dirname() const
Returns the directory part of the filename.
Definition filename.I:358
TypeHandle
TypeHandle is the identifier used to differentiate C++ class types.
Definition typeHandle.h:81
config_daeegg.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
daeMaterials.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
fcollada_utils.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
filename.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
trim
string trim(const string &str)
Returns a new string representing the contents of the given string with both leading and trailing whi...
Definition string_utils.cxx:281
string_utils.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.