未验证 提交 65a6875d 编写于 作者: M Mr.doob 提交者: GitHub

Merge pull request #13707 from takahirox/GLTFLoaderMultiMaterial

GLTFLoader: MultiMaterial support
...@@ -188,7 +188,7 @@ THREE.BufferGeometryUtils = { ...@@ -188,7 +188,7 @@ THREE.BufferGeometryUtils = {
* @param {Array<THREE.BufferGeometry>} geometries * @param {Array<THREE.BufferGeometry>} geometries
* @return {THREE.BufferGeometry} * @return {THREE.BufferGeometry}
*/ */
mergeBufferGeometries: function ( geometries ) { mergeBufferGeometries: function ( geometries, useGroups ) {
var isIndexed = geometries[ 0 ].index !== null; var isIndexed = geometries[ 0 ].index !== null;
...@@ -200,6 +200,8 @@ THREE.BufferGeometryUtils = { ...@@ -200,6 +200,8 @@ THREE.BufferGeometryUtils = {
var mergedGeometry = new THREE.BufferGeometry(); var mergedGeometry = new THREE.BufferGeometry();
var offset = 0;
for ( var i = 0; i < geometries.length; ++ i ) { for ( var i = 0; i < geometries.length; ++ i ) {
var geometry = geometries[ i ]; var geometry = geometries[ i ];
...@@ -242,6 +244,30 @@ THREE.BufferGeometryUtils = { ...@@ -242,6 +244,30 @@ THREE.BufferGeometryUtils = {
} }
if ( useGroups ) {
var count;
if ( isIndexed ) {
count = geometry.index.count;
} else if ( geometry.attributes.position !== undefined ) {
count = geometry.attributes.position.count;
} else {
return null;
}
mergedGeometry.addGroup( offset, count, i );
offset += count;
}
} }
// merge indices // merge indices
......
...@@ -1214,15 +1214,11 @@ THREE.GLTFLoader = ( function () { ...@@ -1214,15 +1214,11 @@ THREE.GLTFLoader = ( function () {
/** /**
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
* *
* @param {THREE.Mesh} mesh * @param {THREE.Geometry} geometry
* @param {GLTF.Mesh} meshDef * @param {Array<GLTF.Target>} targets
* @param {GLTF.Primitive} primitiveDef
* @param {Array<THREE.BufferAttribute>} accessors * @param {Array<THREE.BufferAttribute>} accessors
*/ */
function addMorphTargets( mesh, meshDef, primitiveDef, accessors ) { function addMorphTargets( geometry, targets, accessors ) {
var geometry = mesh.geometry;
var targets = primitiveDef.targets;
var hasMorphPosition = false; var hasMorphPosition = false;
var hasMorphNormal = false; var hasMorphNormal = false;
...@@ -1330,6 +1326,14 @@ THREE.GLTFLoader = ( function () { ...@@ -1330,6 +1326,14 @@ THREE.GLTFLoader = ( function () {
if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions;
if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals;
}
/**
* @param {THREE.Mesh} mesh
* @param {GLTF.Mesh} meshDef
*/
function updateMorphTargets( mesh, meshDef ) {
mesh.updateMorphTargets(); mesh.updateMorphTargets();
if ( meshDef.weights !== undefined ) { if ( meshDef.weights !== undefined ) {
...@@ -1375,26 +1379,31 @@ THREE.GLTFLoader = ( function () { ...@@ -1375,26 +1379,31 @@ THREE.GLTFLoader = ( function () {
} }
var attribA = a.attributes || {}; return isObjectEqual( a.attributes, b.attributes );
var attribB = b.attributes || {};
var keysA = Object.keys( attribA );
var keysB = Object.keys( attribB );
if ( keysA.length !== keysB.length ) { }
return false; function isObjectEqual( a, b ) {
if ( Object.keys( a ).length !== Object.keys( b ).length ) return false;
for ( var key in a ) {
if ( a[ key ] !== b[ key ] ) return false;
} }
for ( var i = 0, il = keysA.length; i < il; i ++ ) { return true;
var key = keysA[ i ]; }
if ( attribA[ key ] !== attribB[ key ] ) { function isArrayEqual( a, b ) {
return false; if ( a.length !== b.length ) return false;
} for ( var i = 0, il = a.length; i < il; i ++ ) {
if ( a[ i ] !== b[ i ] ) return false;
} }
...@@ -1408,11 +1417,35 @@ THREE.GLTFLoader = ( function () { ...@@ -1408,11 +1417,35 @@ THREE.GLTFLoader = ( function () {
var cached = cache[ i ]; var cached = cache[ i ];
if ( isPrimitiveEqual( cached.primitive, newPrimitive ) ) { if ( isPrimitiveEqual( cached.primitive, newPrimitive ) ) return cached.promise;
return cached.promise; }
} return null;
}
function getCachedCombinedGeometry( cache, geometries ) {
for ( var i = 0, il = cache.length; i < il; i ++ ) {
var cached = cache[ i ];
if ( isArrayEqual( geometries, cached.baseGeometries ) ) return cached.geometry;
}
return null;
}
function getCachedMultiPassGeometry( cache, geometry, primitives ) {
for ( var i = 0, il = cache.length; i < il; i ++ ) {
var cached = cache[ i ];
if ( geometry === cached.baseGeometry && isArrayEqual( primitives, cached.primitives ) ) return cached.geometry;
} }
...@@ -1445,6 +1478,47 @@ THREE.GLTFLoader = ( function () { ...@@ -1445,6 +1478,47 @@ THREE.GLTFLoader = ( function () {
} }
/**
* Checks if we can build a single Mesh with MultiMaterial from multiple primitives.
* Returns true if all primitives use the same attributes/morphAttributes/mode
* and also have index. Otherwise returns false.
*
* @param {Array<GLTF.Primitive>} primitives
* @return {Boolean}
*/
function isMultiPassGeometry( primitives ) {
if ( primitives.length < 2 ) return false;
var primitive0 = primitives[ 0 ];
var targets0 = primitive0.targets || [];
if ( primitive0.indices === undefined ) return false;
for ( var i = 1, il = primitives.length; i < il; i ++ ) {
var primitive = primitives[ i ];
if ( primitive0.mode !== primitive.mode ) return false;
if ( primitive.indices === undefined ) return false;
if ( ! isObjectEqual( primitive0.attributes, primitive.attributes ) ) return false;
var targets = primitive.targets || [];
if ( targets0.length !== targets.length ) return false;
for ( var j = 0, jl = targets0.length; j < jl; j ++ ) {
if ( ! isObjectEqual( targets0[ j ], targets[ j ] ) ) return false;
}
}
return true;
}
/* GLTF PARSER */ /* GLTF PARSER */
function GLTFParser( json, extensions, options ) { function GLTFParser( json, extensions, options ) {
...@@ -1458,6 +1532,8 @@ THREE.GLTFLoader = ( function () { ...@@ -1458,6 +1532,8 @@ THREE.GLTFLoader = ( function () {
// BufferGeometry caching // BufferGeometry caching
this.primitiveCache = []; this.primitiveCache = [];
this.multiplePrimitivesCache = [];
this.multiPassGeometryCache = []
this.textureLoader = new THREE.TextureLoader( this.options.manager ); this.textureLoader = new THREE.TextureLoader( this.options.manager );
this.textureLoader.setCrossOrigin( this.options.crossOrigin ); this.textureLoader.setCrossOrigin( this.options.crossOrigin );
...@@ -2183,7 +2259,7 @@ THREE.GLTFLoader = ( function () { ...@@ -2183,7 +2259,7 @@ THREE.GLTFLoader = ( function () {
* @param {GLTF.Primitive} primitiveDef * @param {GLTF.Primitive} primitiveDef
* @param {Array<THREE.BufferAttribute>} accessors * @param {Array<THREE.BufferAttribute>} accessors
*/ */
function addPrimitiveAttributes ( geometry, primitiveDef, accessors ) { function addPrimitiveAttributes( geometry, primitiveDef, accessors ) {
var attributes = primitiveDef.attributes; var attributes = primitiveDef.attributes;
...@@ -2200,16 +2276,33 @@ THREE.GLTFLoader = ( function () { ...@@ -2200,16 +2276,33 @@ THREE.GLTFLoader = ( function () {
} }
if ( primitiveDef.indices !== undefined && !geometry.index ) { if ( primitiveDef.indices !== undefined && ! geometry.index ) {
geometry.setIndex( accessors[ primitiveDef.indices ] ); geometry.setIndex( accessors[ primitiveDef.indices ] );
} }
if ( primitiveDef.targets !== undefined ) {
addMorphTargets( geometry, primitiveDef.targets, accessors );
}
if ( primitiveDef.extras !== undefined ) {
geometry.userData = primitiveDef.extras;
}
} }
/** /**
* Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
*
* Creates BufferGeometries from primitives.
* If we can build a single BufferGeometry with .groups from multiple primitives, returns one BufferGeometry.
* Otherwise, returns BufferGeometries without .groups as many as primitives.
*
* @param {Array<Object>} primitives * @param {Array<Object>} primitives
* @return {Promise<Array<THREE.BufferGeometry>>} * @return {Promise<Array<THREE.BufferGeometry>>}
*/ */
...@@ -2219,6 +2312,22 @@ THREE.GLTFLoader = ( function () { ...@@ -2219,6 +2312,22 @@ THREE.GLTFLoader = ( function () {
var extensions = this.extensions; var extensions = this.extensions;
var cache = this.primitiveCache; var cache = this.primitiveCache;
var isMultiPass = isMultiPassGeometry( primitives );
var originalPrimitives;
if ( isMultiPass ) {
originalPrimitives = primitives; // save original primitives and use later
// We build a single BufferGeometry with .groups from multiple primitives
// because all primitives share the same attributes/morph/mode and have indices.
primitives = [ primitives[ 0 ] ];
// Sets .groups and combined indices to a geometry later in this method.
}
return this.getDependencies( 'accessor' ).then( function ( accessors ) { return this.getDependencies( 'accessor' ).then( function ( accessors ) {
var pending = []; var pending = [];
...@@ -2262,12 +2371,7 @@ THREE.GLTFLoader = ( function () { ...@@ -2262,12 +2371,7 @@ THREE.GLTFLoader = ( function () {
var geometryPromise = Promise.resolve( geometry ); var geometryPromise = Promise.resolve( geometry );
// Cache this geometry // Cache this geometry
cache.push( { cache.push( { primitive: primitive, promise: geometryPromise } );
primitive: primitive,
promise: geometryPromise
} );
pending.push( geometryPromise ); pending.push( geometryPromise );
...@@ -2275,7 +2379,83 @@ THREE.GLTFLoader = ( function () { ...@@ -2275,7 +2379,83 @@ THREE.GLTFLoader = ( function () {
} }
return Promise.all( pending ); return Promise.all( pending ).then( function ( geometries ) {
if ( isMultiPass ) {
var baseGeometry = geometries[ 0 ];
// See if we've already created this combined geometry
var cache = parser.multiPassGeometryCache;
var cached = getCachedMultiPassGeometry( cache, baseGeometry, originalPrimitives );
if ( cached !== null ) return [ cached.geometry ];
// Cloning geometry because of index override.
// Attributes can be reused so cloning by myself here.
var geometry = new THREE.BufferGeometry();
geometry.name = baseGeometry.name;
geometry.userData = baseGeometry.userData;
for ( var key in baseGeometry.attributes ) geometry.addAttribute( key, baseGeometry.attributes[ key ] );
for ( var key in baseGeometry.morphAttributes ) geometry.morphAttributes[ key ] = baseGeometry.morphAttributes[ key ];
var indices = [];
var offset = 0;
for ( var i = 0, il = originalPrimitives.length; i < il; i ++ ) {
var accessor = accessors[ originalPrimitives[ i ].indices ];
for ( var j = 0, jl = accessor.count; j < jl; j ++ ) indices.push( accessor.array[ j ] );
geometry.addGroup( offset, accessor.count, i );
offset += accessor.count;
}
geometry.setIndex( indices );
cache.push( { geometry: geometry, baseGeometry: baseGeometry, primitives: originalPrimitives } );
return [ geometry ];
} else if ( geometries.length > 1 && THREE.BufferGeometryUtils !== undefined ) {
// Tries to merge geometries with BufferGeometryUtils if possible
for ( var i = 1, il = primitives.length; i < il; i ++ ) {
// can't merge if draw mode is different
if ( primitives[ 0 ].mode !== primitives[ i ].mode ) return geometries;
}
// See if we've already created this combined geometry
var cache = parser.multiplePrimitivesCache;
var cached = getCachedCombinedGeometry( cache, geometries );
if ( cached ) {
if ( cached.geometry !== null ) return [ cached.geometry ];
} else {
var geometry = THREE.BufferGeometryUtils.mergeBufferGeometries( geometries, true );
cache.push( { geometry: geometry, baseGeometries: geometries } );
if ( geometry !== null ) return [ geometry ];
}
}
return geometries;
} );
} ); } );
...@@ -2301,188 +2481,220 @@ THREE.GLTFLoader = ( function () { ...@@ -2301,188 +2481,220 @@ THREE.GLTFLoader = ( function () {
] ).then( function ( dependencies ) { ] ).then( function ( dependencies ) {
var group = new THREE.Group();
var primitives = meshDef.primitives; var primitives = meshDef.primitives;
var originalMaterials = [];
for ( var i = 0, il = primitives.length; i < il; i ++ ) {
originalMaterials[ i ] = primitives[ i ].material === undefined
? createDefaultMaterial()
: dependencies.materials[ primitives[ i ].material ];
}
return scope.loadGeometries( primitives ).then( function ( geometries ) { return scope.loadGeometries( primitives ).then( function ( geometries ) {
for ( var i = 0, il = primitives.length; i < il; i ++ ) { var isMultiMaterial = geometries.length === 1 && geometries[ 0 ].groups.length > 0;
var primitive = primitives[ i ]; var meshes = [];
var geometry = geometries[ i ];
var material = primitive.material === undefined for ( var i = 0, il = geometries.length; i < il; i ++ ) {
? createDefaultMaterial()
: dependencies.materials[ primitive.material ];
if ( material.aoMap var geometry = geometries[ i ];
&& geometry.attributes.uv2 === undefined var primitive = primitives[ i ];
&& geometry.attributes.uv !== undefined ) {
console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' ); // 1. create Mesh
geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) );
} var mesh;
// If the material will be modified later on, clone it now. var material = isMultiMaterial ? originalMaterials : originalMaterials[ i ]
var useVertexColors = geometry.attributes.color !== undefined;
var useFlatShading = geometry.attributes.normal === undefined;
var useSkinning = meshDef.isSkinnedMesh === true;
var useMorphTargets = primitive.targets !== undefined;
if ( useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
primitive.mode === undefined ) {
if ( material.isGLTFSpecularGlossinessMaterial ) { // .isSkinnedMesh isn't in glTF spec. See .markDefs()
mesh = meshDef.isSkinnedMesh === true
? new THREE.SkinnedMesh( geometry, material )
: new THREE.Mesh( geometry, material );
var specGlossExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {
material = specGlossExtension.cloneMaterial( material );
} else { mesh.drawMode = THREE.TriangleStripDrawMode;
material = material.clone(); } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) {
mesh.drawMode = THREE.TriangleFanDrawMode;
} }
} } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {
mesh = new THREE.LineSegments( geometry, material );
} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) {
mesh = new THREE.Line( geometry, material );
} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {
mesh = new THREE.LineLoop( geometry, material );
} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) {
mesh = new THREE.Points( geometry, material );
if ( useVertexColors ) { } else {
material.vertexColors = THREE.VertexColors; throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode );
material.needsUpdate = true;
} }
if ( useFlatShading ) { if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) {
material.flatShading = true; updateMorphTargets( mesh, meshDef );
} }
var mesh; mesh.name = meshDef.name || ( 'mesh_' + meshIndex );
if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || if ( geometries.length > 1 ) mesh.name += '_' + i;
primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
primitive.mode === undefined ) {
if ( useSkinning ) { if ( meshDef.extras !== undefined ) mesh.userData = meshDef.extras;
mesh = new THREE.SkinnedMesh( geometry, material ); meshes.push( mesh );
material.skinning = true;
} else { // 2. update Material depending on Mesh and BufferGeometry
mesh = new THREE.Mesh( geometry, material ); var materials = isMultiMaterial ? mesh.material : [ mesh.material ];
} var useVertexColors = geometry.attributes.color !== undefined;
var useFlatShading = geometry.attributes.normal === undefined;
var useSkinning = mesh.isSkinnedMesh === true;
var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0;
var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined;
if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { for ( var j = 0, jl = materials.length; j < jl; j ++ ) {
mesh.drawMode = THREE.TriangleStripDrawMode; var material = materials[ j ];
} else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { if ( mesh.isPoints ) {
mesh.drawMode = THREE.TriangleFanDrawMode; var cacheKey = 'PointsMaterial:' + material.uuid;
} var pointsMaterial = scope.cache.get( cacheKey );
} else if ( primitive.mode === WEBGL_CONSTANTS.LINES || if ( ! pointsMaterial ) {
primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ||
primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {
var cacheKey = 'LineBasicMaterial:' + material.uuid; pointsMaterial = new THREE.PointsMaterial();
THREE.Material.prototype.copy.call( pointsMaterial, material );
pointsMaterial.color.copy( material.color );
pointsMaterial.map = material.map;
pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet
var lineMaterial = scope.cache.get( cacheKey ); scope.cache.add( cacheKey, pointsMaterial );
if ( ! lineMaterial ) { }
lineMaterial = new THREE.LineBasicMaterial(); material = pointsMaterial;
THREE.Material.prototype.copy.call( lineMaterial, material );
lineMaterial.color.copy( material.color );
lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet
scope.cache.add( cacheKey, lineMaterial ); } else if ( mesh.isLine ) {
} var cacheKey = 'LineBasicMaterial:' + material.uuid;
material = lineMaterial; var lineMaterial = scope.cache.get( cacheKey );
if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { if ( ! lineMaterial ) {
mesh = new THREE.LineSegments( geometry, material ); lineMaterial = new THREE.LineBasicMaterial();
THREE.Material.prototype.copy.call( lineMaterial, material );
lineMaterial.color.copy( material.color );
lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet
} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { scope.cache.add( cacheKey, lineMaterial );
mesh = new THREE.Line( geometry, material ); }
} else { material = lineMaterial;
}
// If the material will be modified later on, clone it now.
if ( useVertexColors || useFlatShading || useSkinning || useMorphTargets ) {
mesh = new THREE.LineLoop( geometry, material ); material = material.isGLTFSpecularGlossinessMaterial
? extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].cloneMaterial( material )
: material.clone();
} }
} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { if ( useSkinning ) {
var cacheKey = 'PointsMaterial:' + material.uuid; material.skinning = true;
var pointsMaterial = scope.cache.get( cacheKey ); }
if ( ! pointsMaterial ) { if ( useVertexColors ) {
material.vertexColors = THREE.VertexColors;
material.needsUpdate = true;
}
pointsMaterial = new THREE.PointsMaterial(); if ( useFlatShading ) {
THREE.Material.prototype.copy.call( pointsMaterial, material );
pointsMaterial.color.copy( material.color );
pointsMaterial.map = material.map;
pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet
scope.cache.add( cacheKey, pointsMaterial ); material.flatShading = true;
} }
material = pointsMaterial; if ( useMorphTargets ) {
mesh = new THREE.Points( geometry, material ); material.morphTargets = true;
} else { }
throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); if ( useMorphNormals ) {
} material.morphNormals = true;
mesh.name = meshDef.name || ( 'mesh_' + meshIndex ); }
if ( useMorphTargets ) { materials[ j ] = material;
addMorphTargets( mesh, meshDef, primitive, dependencies.accessors ); // workarounds for mesh and geometry
material.morphTargets = true; if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) {
if ( mesh.geometry.morphAttributes.normal !== undefined ) material.morphNormals = true; console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' );
geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) );
} }
if ( meshDef.extras !== undefined ) mesh.userData = meshDef.extras; if ( material.isGLTFSpecularGlossinessMaterial ) {
if ( primitive.extras !== undefined ) mesh.geometry.userData = primitive.extras;
// for Specular-Glossiness. // for GLTFSpecularGlossinessMaterial(ShaderMaterial) uniforms runtime update
if ( material.isGLTFSpecularGlossinessMaterial === true ) { mesh.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms;
mesh.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms; }
} }
if ( primitives.length > 1 ) { mesh.material = isMultiMaterial ? materials : materials[ 0 ];
mesh.name += '_' + i; }
group.add( mesh ); if ( meshes.length === 1 ) {
} else { return meshes[ 0 ];
return mesh; }
} var group = new THREE.Group();
for ( var i = 0, il = meshes.length; i < il; i ++ ) {
group.add( meshes[ i ] );
} }
...@@ -2726,6 +2938,7 @@ THREE.GLTFLoader = ( function () { ...@@ -2726,6 +2938,7 @@ THREE.GLTFLoader = ( function () {
var node; var node;
// .isBone isn't in glTF spec. See .markDefs
if ( nodeDef.isBone === true ) { if ( nodeDef.isBone === true ) {
node = new THREE.Bone(); node = new THREE.Bone();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册