From 0ccb67b625fa11e04b480c39ecfc12800b2d4117 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Mon, 1 Jun 2026 14:33:23 +0200 Subject: [PATCH 1/2] Editor: Fix name conflict during glTF export. (#33699) --- editor/js/Animation.js | 21 +++++-- editor/js/Menubar.File.js | 121 +++++++++++++++++++++++++++++++++++++- editor/js/Strings.js | 6 ++ 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/editor/js/Animation.js b/editor/js/Animation.js index adad37911cac20..93748454ba6a27 100644 --- a/editor/js/Animation.js +++ b/editor/js/Animation.js @@ -367,7 +367,14 @@ function Animation( editor ) { clipRow.addEventListener( 'click', function () { - editor.select( root ); + if ( editor.selected !== root ) { + + signals.objectSelected.remove( selectDefaultClip ); + editor.select( root ); + signals.objectSelected.add( selectDefaultClip ); + + } + selectClip( clip, root ); update(); // Refresh to update highlighting @@ -578,10 +585,7 @@ function Animation( editor ) { } - updateTime(); - - // Auto-select clip when an object with animations is selected - signals.objectSelected.add( function ( object ) { + function selectDefaultClip( object ) { if ( object !== null && object.animations && object.animations.length > 0 ) { @@ -590,7 +594,12 @@ function Animation( editor ) { } - } ); + } + + updateTime(); + + // Auto-select clip when an object with animations is selected + signals.objectSelected.add( selectDefaultClip ); // Update when scene changes signals.editorCleared.add( clear ); diff --git a/editor/js/Menubar.File.js b/editor/js/Menubar.File.js index 298f773348fb3f..c0708f9e468fec 100644 --- a/editor/js/Menubar.File.js +++ b/editor/js/Menubar.File.js @@ -1,5 +1,5 @@ import { UIPanel, UIRow, UIHorizontalRule } from './libs/ui.js'; -import { FileLoader } from 'three'; +import { FileLoader, PropertyBinding } from 'three'; function MenubarFile( editor ) { @@ -276,6 +276,15 @@ function MenubarFile( editor ) { option.onClick( async function () { const scene = editor.scene; + + if ( needsUniqueNames( scene ) ) { // see #25179 + + if ( confirm( strings.getKey( 'prompt/file/export/duplicateNames' ) ) === false ) return; + + ensureUniqueNames( scene ); + + } + const animations = getAnimations( scene ); const optimizedAnimations = []; @@ -307,6 +316,15 @@ function MenubarFile( editor ) { option.onClick( async function () { const scene = editor.scene; + + if ( needsUniqueNames( scene ) ) { // see #25179 + + if ( confirm( strings.getKey( 'prompt/file/export/duplicateNames' ) ) === false ) return; + + ensureUniqueNames( scene ); + + } + const animations = getAnimations( scene ); const optimizedAnimations = []; @@ -460,6 +478,107 @@ function MenubarFile( editor ) { } + function needsUniqueNames( scene ) { + + const usedNames = new Set(); + let duplicate = false; + let animated = false; + + scene.traverse( function ( object ) { + + if ( object.animations.length > 0 ) animated = true; + + if ( object.name === '' ) return; + + if ( usedNames.has( object.name ) ) duplicate = true; + + usedNames.add( object.name ); + + } ); + + return duplicate && animated; + + } + + // Gives every object a unique name and keeps the animation tracks that + // reference them by name in sync. The renamed scene mirrors the result of a + // glTF round-trip, where the loader makes all names unique, too. + + function ensureUniqueNames( scene ) { + + // Resolve each track's target object up front, scoped to the object that + // owns the clip. This disambiguates colliding names before they change. + + const trackBindings = []; + + scene.traverse( function ( owner ) { + + for ( const clip of owner.animations ) { + + for ( const track of clip.tracks ) { + + const nodeName = PropertyBinding.parseTrackName( track.name ).nodeName; + const target = PropertyBinding.findNode( owner, nodeName ); + + // References by UUID stay valid, so only track name-based ones. + + if ( target !== null && target.name === nodeName ) { + + trackBindings.push( { track, target, nodeName } ); + + } + + } + + } + + } ); + + // Assign a unique name to every named object. + + let changed = false; + const usedNames = new Set(); + + scene.traverse( function ( object ) { + + if ( object.name === '' ) return; + + if ( usedNames.has( object.name ) ) { + + let suffix = 1, name; + do { + + name = object.name + '_' + ( suffix ++ ); + + } while ( usedNames.has( name ) ); + + object.name = name; + changed = true; + + } + + usedNames.add( object.name ); + + } ); + + if ( changed === false ) return; + + // Point the affected tracks at their renamed targets. + + for ( const { track, target, nodeName } of trackBindings ) { + + if ( target.name !== nodeName ) { + + track.name = target.name + track.name.slice( nodeName.length ); + + } + + } + + editor.signals.sceneGraphChanged.dispatch(); + + } + return container; } diff --git a/editor/js/Strings.js b/editor/js/Strings.js index c012af3d4aa7d5..3246f8cb513fe5 100644 --- a/editor/js/Strings.js +++ b/editor/js/Strings.js @@ -8,6 +8,7 @@ function Strings( config ) { 'prompt/file/failedToOpenProject': 'خطایی در باز کردن پروژه پیش آمده', 'prompt/file/export/noMeshSelected': 'هیچ Mesh ای انتخاب نکردید', 'prompt/file/export/noObjectSelected': 'هیچ آبجکتی انتخاب نکردید!', + 'prompt/file/export/duplicateNames': 'Some objects share the same name. They will be renamed to ensure unique names. Are you sure?', 'prompt/script/remove': 'آیا اطمینان دارید؟', 'prompt/history/clear': 'هیستوری قبل و بعد (undo / redo) پاک خواهند شد آیا مطمئنید؟', 'prompt/history/preserve': 'The history will be preserved across sessions.\nThis can have an impact on performance when working with textures.', @@ -456,6 +457,7 @@ function Strings( config ) { 'prompt/file/failedToOpenProject': 'Failed to open project!', 'prompt/file/export/noMeshSelected': 'No Mesh selected!', 'prompt/file/export/noObjectSelected': 'No Object selected!', + 'prompt/file/export/duplicateNames': 'Some objects share the same name. They will be renamed to ensure unique names. Are you sure?', 'prompt/script/remove': 'Are you sure?', 'prompt/history/clear': 'The Undo/Redo History will be cleared. Are you sure?', 'prompt/history/preserve': 'The history will be preserved across sessions.\nThis can have an impact on performance when working with textures.', @@ -905,6 +907,7 @@ function Strings( config ) { 'prompt/file/failedToOpenProject': 'Échec de l\'ouverture du projet !', 'prompt/file/export/noMeshSelected': 'Aucun maillage sélectionné !', 'prompt/file/export/noObjectSelected': 'Aucun objet sélectionné !', + 'prompt/file/export/duplicateNames': 'Certains objets portent le même nom. Ils seront renommés afin de garantir des noms uniques. Êtes-vous sûr ?', 'prompt/script/remove': 'Es-tu sûr?', 'prompt/history/clear': 'L\'historique d\'annulation/rétablissement sera effacé Êtes-vous sûr ?', 'prompt/history/preserve': 'L\'histoire sera conservée entre les sessions.\nCela peut avoir un impact sur les performances lors de la manipulation des textures.', @@ -1354,6 +1357,7 @@ function Strings( config ) { 'prompt/file/failedToOpenProject': '无法打开项目!', 'prompt/file/export/noMeshSelected': '未选择网格!', 'prompt/file/export/noObjectSelected': '未选择对象!', + 'prompt/file/export/duplicateNames': '部分对象具有相同的名称。它们将被重命名以确保名称唯一。确定吗?', 'prompt/script/remove': '你确定吗?', 'prompt/history/clear': '撤销/重做历史记录将被清除。您确定吗?', 'prompt/history/preserve': '历史将在会话之间保留。\n这可能会影响在处理纹理时的性能。', @@ -1803,6 +1807,7 @@ function Strings( config ) { 'prompt/file/failedToOpenProject': 'プロジェクトを開くことができませんでした!', 'prompt/file/export/noMeshSelected': 'メッシュが選択されていません!', 'prompt/file/export/noObjectSelected': 'オブジェクトが選択されていません!', + 'prompt/file/export/duplicateNames': '一部のオブジェクトの名前が重複しています。名前を一意にするために変更します。よろしいですか?', 'prompt/script/remove': '本気ですか?', 'prompt/history/clear': '元に戻す/やり直しの履歴が消去されます。 本気ですか?', 'prompt/history/preserve': '履歴はセッションをまたいで保存されます。\nこれは、テクスチャを操作する際のパフォーマンスに影響を与える可能性があります。', @@ -2251,6 +2256,7 @@ function Strings( config ) { 'prompt/file/failedToOpenProject': '프로젝트를 여는 데 실패했습니다!', 'prompt/file/export/noMeshSelected': '메시가 선택되지 않았습니다!', 'prompt/file/export/noObjectSelected': '객체가 선택되지 않았습니다!', + 'prompt/file/export/duplicateNames': '일부 객체의 이름이 중복됩니다. 이름을 고유하게 만들기 위해 변경됩니다. 계속하시겠습니까?', 'prompt/script/remove': '삭제하시겠습니까?', 'prompt/history/clear': '되돌리기/다시하기 기록이 지워집니다. 진행하시겠습니까?', 'prompt/history/preserve': '기록은 세션을 통해 저장됩니다. 이는 텍스처를 조작할 때 성능에 영향을 미칠 수 있습니다.', From 7ad644be80370c3ecbe2fdc28d4c1296c0a0e463 Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 1 Jun 2026 13:44:38 -0300 Subject: [PATCH 2/2] TSL: Migrate vertex accessor classes to TSL Fn (#33674) --- .../webgpu_postprocessing_motion_blur.jpg | Bin 62524 -> 63240 bytes src/materials/nodes/NodeMaterial.js | 22 +- src/nodes/Nodes.js | 5 - src/nodes/TSL.js | 9 +- src/nodes/accessors/Batch.js | 108 ++++++ src/nodes/accessors/BatchNode.js | 163 -------- src/nodes/accessors/Instance.js | 271 +++++++++++++ src/nodes/accessors/InstanceNode.js | 367 ------------------ src/nodes/accessors/InstancedMeshNode.js | 50 --- .../accessors/{MorphNode.js => Morph.js} | 206 +++++----- src/nodes/accessors/Skinning.js | 263 +++++++++++++ src/nodes/accessors/SkinningNode.js | 328 ---------------- src/objects/InstancedMesh.js | 11 - src/objects/Skeleton.js | 9 - src/renderers/common/RenderObject.js | 2 +- 15 files changed, 745 insertions(+), 1069 deletions(-) create mode 100644 src/nodes/accessors/Batch.js delete mode 100644 src/nodes/accessors/BatchNode.js create mode 100644 src/nodes/accessors/Instance.js delete mode 100644 src/nodes/accessors/InstanceNode.js delete mode 100644 src/nodes/accessors/InstancedMeshNode.js rename src/nodes/accessors/{MorphNode.js => Morph.js} (56%) create mode 100644 src/nodes/accessors/Skinning.js delete mode 100644 src/nodes/accessors/SkinningNode.js diff --git a/examples/screenshots/webgpu_postprocessing_motion_blur.jpg b/examples/screenshots/webgpu_postprocessing_motion_blur.jpg index 0d748b68e188e30f47d04f345fac049719472d78..2e2b02578ba7f9264f9bab91471a87933fb31c03 100644 GIT binary patch delta 38536 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 Ub2@}^xse?K8R$hFK>q;$*$==A_5c6? delta 37815 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 Kg&=2e(q8Gq8>1} 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 diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index 13f990fd320111..5963e91506ab43 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -1,16 +1,16 @@ import { Material } from '../Material.js'; import { hashArray, hashString } from '../../nodes/core/NodeUtils.js'; -import { output, diffuseColor, emissive, varyingProperty } from '../../nodes/core/PropertyNode.js'; +import { output, diffuseColor, emissive } from '../../nodes/core/PropertyNode.js'; import { materialAlphaTest, materialColor, materialOpacity, materialEmissive, materialNormal, materialLightMap, materialAO } from '../../nodes/accessors/MaterialNode.js'; import { modelViewProjection } from '../../nodes/accessors/ModelViewProjectionNode.js'; import { normalLocal } from '../../nodes/accessors/Normal.js'; -import { instancedMesh } from '../../nodes/accessors/InstancedMeshNode.js'; -import { batch } from '../../nodes/accessors/BatchNode.js'; +import { instancedMesh, instanceColor } from '../../nodes/accessors/Instance.js'; +import { batch, batchColor } from '../../nodes/accessors/Batch.js'; import { materialReference } from '../../nodes/accessors/MaterialReferenceNode.js'; import { positionLocal, positionView } from '../../nodes/accessors/Position.js'; -import { skinning } from '../../nodes/accessors/SkinningNode.js'; -import { morphReference } from '../../nodes/accessors/MorphNode.js'; +import { skinning } from '../../nodes/accessors/Skinning.js'; +import { morphReference } from '../../nodes/accessors/Morph.js'; import { fwidth, mix, smoothstep } from '../../nodes/math/MathNode.js'; import { float, vec3, vec4, bool } from '../../nodes/tsl/TSLBase.js'; import AONode from '../../nodes/lighting/AONode.js'; @@ -766,13 +766,13 @@ class NodeMaterial extends Material { if ( geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color ) { - morphReference( object ).toStack(); + morphReference( object ); } if ( object.isSkinnedMesh === true ) { - skinning( object ).toStack(); + skinning( object ); } @@ -788,13 +788,13 @@ class NodeMaterial extends Material { if ( object.isBatchedMesh ) { - batch( object ).toStack(); + batch( object ); } if ( ( object.isInstancedMesh && object.instanceMatrix && object.instanceMatrix.isInstancedBufferAttribute === true ) ) { - instancedMesh( object ).toStack(); + instancedMesh( object ); } @@ -844,16 +844,12 @@ class NodeMaterial extends Material { if ( object.instanceColor ) { - const instanceColor = varyingProperty( 'vec3', 'vInstanceColor' ); - colorNode = instanceColor.mul( colorNode ); } if ( object.isBatchedMesh && object._colorsTexture ) { - const batchColor = varyingProperty( 'vec3', 'vBatchColor' ); - colorNode = batchColor.mul( colorNode ); } diff --git a/src/nodes/Nodes.js b/src/nodes/Nodes.js index e29383bfe31500..82f7a49e80c69d 100644 --- a/src/nodes/Nodes.js +++ b/src/nodes/Nodes.js @@ -43,24 +43,19 @@ import * as NodeUtils from './core/NodeUtils.js'; export { NodeUtils }; // accessors -export { default as BatchNode } from './accessors/BatchNode.js'; export { default as BufferAttributeNode } from './accessors/BufferAttributeNode.js'; export { default as BufferNode } from './accessors/BufferNode.js'; export { default as BuiltinNode } from './accessors/BuiltinNode.js'; export { default as ClippingNode } from './accessors/ClippingNode.js'; export { default as CubeTextureNode } from './accessors/CubeTextureNode.js'; -export { default as InstanceNode } from './accessors/InstanceNode.js'; -export { default as InstancedMeshNode } from './accessors/InstancedMeshNode.js'; export { default as MaterialNode } from './accessors/MaterialNode.js'; export { default as MaterialReferenceNode } from './accessors/MaterialReferenceNode.js'; export { default as ModelNode } from './accessors/ModelNode.js'; -export { default as MorphNode } from './accessors/MorphNode.js'; export { default as Object3DNode } from './accessors/Object3DNode.js'; export { default as PointUVNode } from './accessors/PointUVNode.js'; export { default as ReferenceBaseNode } from './accessors/ReferenceBaseNode.js'; export { default as ReferenceNode } from './accessors/ReferenceNode.js'; export { default as RendererReferenceNode } from './accessors/RendererReferenceNode.js'; -export { default as SkinningNode } from './accessors/SkinningNode.js'; export { default as StorageBufferNode } from './accessors/StorageBufferNode.js'; export { default as StorageTexture3DNode } from './accessors/StorageTexture3DNode.js'; export { default as StorageTextureNode } from './accessors/StorageTextureNode.js'; diff --git a/src/nodes/TSL.js b/src/nodes/TSL.js index 9ba06eea730ee8..fc3997d18bcfc3 100644 --- a/src/nodes/TSL.js +++ b/src/nodes/TSL.js @@ -63,14 +63,13 @@ export * from './accessors/BuiltinNode.js'; export * from './accessors/Camera.js'; export * from './accessors/VertexColorNode.js'; export * from './accessors/CubeTextureNode.js'; -export * from './accessors/InstanceNode.js'; -export * from './accessors/InstancedMeshNode.js'; -export * from './accessors/BatchNode.js'; +export * from './accessors/Instance.js'; +export * from './accessors/Batch.js'; export * from './accessors/MaterialNode.js'; export * from './accessors/MaterialProperties.js'; export * from './accessors/MaterialReferenceNode.js'; export * from './accessors/RendererReferenceNode.js'; -export * from './accessors/MorphNode.js'; +export * from './accessors/Morph.js'; export * from './accessors/TextureBicubic.js'; export * from './accessors/ModelNode.js'; export * from './accessors/ModelViewProjectionNode.js'; @@ -80,7 +79,7 @@ export * from './accessors/PointUVNode.js'; export * from './accessors/Position.js'; export * from './accessors/ReferenceNode.js'; export * from './accessors/ReflectVector.js'; -export * from './accessors/SkinningNode.js'; +export * from './accessors/Skinning.js'; export * from './accessors/SceneProperties.js'; export * from './accessors/StorageBufferNode.js'; export * from './accessors/StorageTexture3DNode.js'; diff --git a/src/nodes/accessors/Batch.js b/src/nodes/accessors/Batch.js new file mode 100644 index 00000000000000..20a9d48a3c94fe --- /dev/null +++ b/src/nodes/accessors/Batch.js @@ -0,0 +1,108 @@ + +import { normalLocal } from './Normal.js'; +import { positionLocal } from './Position.js'; +import { vec3, mat3, mat4, int, ivec2, float, Fn } from '../tsl/TSLBase.js'; +import { textureLoad } from './TextureNode.js'; +import { textureSize } from './TextureSizeNode.js'; +import { tangentLocal } from './Tangent.js'; +import { instanceIndex, drawIndex } from '../core/IndexNode.js'; +import { varyingProperty } from '../core/PropertyNode.js'; + +/** + * TSL function that retrieves the batching color for a given instance ID from a colors texture. + * + * @param {Node} colorsTexture - The colors texture. + * @param {Node} id - The instance or batch ID. + * @returns {Node} The retrieved color. + */ +const getBatchingColor = /*@__PURE__*/ Fn( ( [ colorsTexture, id ] ) => { + + const size = int( textureSize( textureLoad( colorsTexture ), 0 ).x ).toConst(); + const j = int( id ); + const x = j.mod( size ).toConst(); + const y = j.div( size ).toConst(); + return textureLoad( colorsTexture, ivec2( x, y ) ).rgb; + +} ); + +/** + * TSL function that retrieves the indirect index for a given batch ID. + * + * @param {BatchedMesh} batchMesh - The batched mesh. + * @param {Node} id - The draw or instance ID. + * @returns {Node} The indirect index. + */ +const getIndirectIndex = /*@__PURE__*/ Fn( ( [ indirectTexture, id ] ) => { + + const size = int( textureSize( textureLoad( indirectTexture ), 0 ).x ).toConst(); + const x = int( id ).mod( size ).toConst(); + const y = int( id ).div( size ).toConst(); + return textureLoad( indirectTexture, ivec2( x, y ) ).x; + +} ); + +/** + * TSL object representing a varying property for the batching color vector. + * + * @type {VaryingNode} + */ +export const batchColor = /*@__PURE__*/ varyingProperty( 'vec3', 'vBatchColor' ); + +/** + * TSL function representing the vertex shader batching setup. + * Applies the batch transformation matrix to positionLocal, normalLocal, and tangentLocal. + * Also assigns the batch color if a color texture is present. + * + * @tsl + * @function + * @param {BatchedMesh} batchMesh - The batched mesh. + */ +export const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { + + const batchingIdNode = builder.getDrawIndex() === null ? instanceIndex : drawIndex; + + const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) ); + + const matricesTexture = batchMesh._matricesTexture; + + const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst(); + const j = float( indirectId ).mul( 4 ).toInt().toConst(); + + const x = j.mod( size ).toConst(); + const y = j.div( size ).toConst(); + const batchingMatrix = mat4( + textureLoad( matricesTexture, ivec2( x, y ) ), + textureLoad( matricesTexture, ivec2( x.add( 1 ), y ) ), + textureLoad( matricesTexture, ivec2( x.add( 2 ), y ) ), + textureLoad( matricesTexture, ivec2( x.add( 3 ), y ) ) + ); + + const colorsTexture = batchMesh._colorsTexture; + + if ( colorsTexture !== null ) { + + const color = getBatchingColor( colorsTexture, indirectId ); + + batchColor.assign( color ); + + } + + const bm = mat3( batchingMatrix ); + + positionLocal.assign( batchingMatrix.mul( positionLocal ) ); + + const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) ); + + const batchingNormal = bm.mul( transformedNormal ).xyz; + + normalLocal.assign( batchingNormal ); + + if ( builder.hasGeometryAttribute( 'tangent' ) ) { + + tangentLocal.mulAssign( bm ); + + } + +}, 'void' ); + + diff --git a/src/nodes/accessors/BatchNode.js b/src/nodes/accessors/BatchNode.js deleted file mode 100644 index 9138e54d3b7089..00000000000000 --- a/src/nodes/accessors/BatchNode.js +++ /dev/null @@ -1,163 +0,0 @@ -import Node from '../core/Node.js'; -import { normalLocal } from './Normal.js'; -import { positionLocal } from './Position.js'; -import { nodeProxy, vec3, mat3, mat4, int, ivec2, float, Fn } from '../tsl/TSLBase.js'; -import { textureLoad } from './TextureNode.js'; -import { textureSize } from './TextureSizeNode.js'; -import { tangentLocal } from './Tangent.js'; -import { instanceIndex, drawIndex } from '../core/IndexNode.js'; -import { varyingProperty } from '../core/PropertyNode.js'; - -/** - * This node implements the vertex shader logic which is required - * when rendering 3D objects via batching. `BatchNode` must be used - * with instances of {@link BatchedMesh}. - * - * @augments Node - */ -class BatchNode extends Node { - - static get type() { - - return 'BatchNode'; - - } - - /** - * Constructs a new batch node. - * - * @param {BatchedMesh} batchMesh - A reference to batched mesh. - */ - constructor( batchMesh ) { - - super( 'void' ); - - /** - * A reference to batched mesh. - * - * @type {BatchedMesh} - */ - this.batchMesh = batchMesh; - - /** - * The batching index node. - * - * @type {?IndexNode} - * @default null - */ - this.batchingIdNode = null; - - } - - /** - * Setups the internal buffers and nodes and assigns the transformed vertex data - * to predefined node variables for accumulation. That follows the same patterns - * like with morph and skinning nodes. - * - * @param {NodeBuilder} builder - The current node builder. - */ - setup( builder ) { - - if ( this.batchingIdNode === null ) { - - if ( builder.getDrawIndex() === null ) { - - this.batchingIdNode = instanceIndex; - - } else { - - this.batchingIdNode = drawIndex; - - } - - } - - const getIndirectIndex = Fn( ( [ id ] ) => { - - const size = int( textureSize( textureLoad( this.batchMesh._indirectTexture ), 0 ).x ).toConst(); - const x = int( id ).mod( size ).toConst(); - const y = int( id ).div( size ).toConst(); - return textureLoad( this.batchMesh._indirectTexture, ivec2( x, y ) ).x; - - } ).setLayout( { - name: 'getIndirectIndex', - type: 'uint', - inputs: [ - { name: 'id', type: 'int' } - ] - } ); - - const indirectId = getIndirectIndex( int( this.batchingIdNode ) ); - - const matricesTexture = this.batchMesh._matricesTexture; - - const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst(); - const j = float( indirectId ).mul( 4 ).toInt().toConst(); - - const x = j.mod( size ).toConst(); - const y = j.div( size ).toConst(); - const batchingMatrix = mat4( - textureLoad( matricesTexture, ivec2( x, y ) ), - textureLoad( matricesTexture, ivec2( x.add( 1 ), y ) ), - textureLoad( matricesTexture, ivec2( x.add( 2 ), y ) ), - textureLoad( matricesTexture, ivec2( x.add( 3 ), y ) ) - ); - - - const colorsTexture = this.batchMesh._colorsTexture; - - if ( colorsTexture !== null ) { - - const getBatchingColor = Fn( ( [ id ] ) => { - - const size = int( textureSize( textureLoad( colorsTexture ), 0 ).x ).toConst(); - const j = id; - const x = j.mod( size ).toConst(); - const y = j.div( size ).toConst(); - return textureLoad( colorsTexture, ivec2( x, y ) ).rgb; - - } ).setLayout( { - name: 'getBatchingColor', - type: 'vec3', - inputs: [ - { name: 'id', type: 'int' } - ] - } ); - - const color = getBatchingColor( indirectId ); - - varyingProperty( 'vec3', 'vBatchColor' ).assign( color ); - - } - - const bm = mat3( batchingMatrix ); - - positionLocal.assign( batchingMatrix.mul( positionLocal ) ); - - const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) ); - - const batchingNormal = bm.mul( transformedNormal ).xyz; - - normalLocal.assign( batchingNormal ); - - if ( builder.hasGeometryAttribute( 'tangent' ) ) { - - tangentLocal.mulAssign( bm ); - - } - - } - -} - -export default BatchNode; - -/** - * TSL function for creating a batch node. - * - * @tsl - * @function - * @param {BatchedMesh} batchMesh - A reference to batched mesh. - * @returns {BatchNode} - */ -export const batch = /*@__PURE__*/ nodeProxy( BatchNode ).setParameterLength( 1 ); diff --git a/src/nodes/accessors/Instance.js b/src/nodes/accessors/Instance.js new file mode 100644 index 00000000000000..b9dbe74da72e07 --- /dev/null +++ b/src/nodes/accessors/Instance.js @@ -0,0 +1,271 @@ + +import { vec3, mat4, Fn } from '../tsl/TSLBase.js'; +import { OnFrameUpdate, OnObjectUpdate } from '../utils/EventNode.js'; +import { normalLocal, transformNormal } from './Normal.js'; +import { positionLocal, positionPrevious } from './Position.js'; +import { varyingProperty } from '../core/PropertyNode.js'; +import { instancedBufferAttribute, instancedDynamicBufferAttribute } from './BufferAttributeNode.js'; +import { buffer } from './BufferNode.js'; +import { storage } from './StorageBufferNode.js'; +import { instanceIndex } from '../core/IndexNode.js'; + +import { InstancedInterleavedBuffer } from '../../core/InstancedInterleavedBuffer.js'; +import { InstancedBufferAttribute } from '../../core/InstancedBufferAttribute.js'; +import { DynamicDrawUsage } from '../../constants.js'; + +const _matrixBuffers = /*@__PURE__*/ new WeakMap(); +const _colorBuffers = /*@__PURE__*/ new WeakMap(); +const _previousInstanceMatrices = /*@__PURE__*/ new WeakMap(); + +/** + * Creates the appropriate node for instanced matrix transformations. + * Depending on buffer limits and storage capability, returns either a storage, buffer, or instanced interleaved attribute node. + * + * @param {NodeBuilder} builder - The current node builder. + * @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceMatrix - The matrix buffer attribute. + * @param {number} count - The instance count. + * @returns {Node} The matrix node. + */ +function createInstanceMatrixNode( builder, instanceMatrix, count ) { + + let instanceMatrixNode; + + const isStorageMatrix = instanceMatrix.isStorageInstancedBufferAttribute === true; + + if ( isStorageMatrix ) { + + instanceMatrixNode = storage( instanceMatrix, 'mat4', Math.max( count, 1 ) ).element( instanceIndex ); + + } else { + + const uniformBufferSize = count * 16 * 4; + + if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { + + instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', Math.max( count, 1 ) ).element( instanceIndex ); + + } else { + + let interleaved = _matrixBuffers.get( instanceMatrix ); + + if ( ! interleaved ) { + + interleaved = new InstancedInterleavedBuffer( instanceMatrix.array, 16, 1 ); + _matrixBuffers.set( instanceMatrix, interleaved ); + + } + + const bufferFn = instanceMatrix.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute; + + const instanceBuffers = [ + bufferFn( interleaved, 'vec4', 16, 0 ), + bufferFn( interleaved, 'vec4', 16, 4 ), + bufferFn( interleaved, 'vec4', 16, 8 ), + bufferFn( interleaved, 'vec4', 16, 12 ) + ]; + + instanceMatrixNode = mat4( ...instanceBuffers ); + + } + + } + + return instanceMatrixNode; + +} + +/** + * Retrieves or initializes the previous frame instance matrix node for motion vectors. + * Uses a WeakMap to cache previous frame instance matrices and their TSL nodes. + * + * @param {InstancedMesh} instancedMesh - The instanced mesh object. + * @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceMatrix - The current matrix buffer attribute. + * @param {NodeBuilder} builder - The current node builder. + * @param {number} count - The instance count. + * @returns {Node} The previous frame instance matrix node. + */ +function getPreviousInstance( instancedMesh, instanceMatrix, builder, count ) { + + let data = _previousInstanceMatrices.get( instancedMesh ); + + if ( data === undefined ) { + + const previousInstanceMatrix = instanceMatrix.clone(); + + data = { + previousInstanceMatrix, + node: createInstanceMatrixNode( builder, previousInstanceMatrix, count ) + }; + + _previousInstanceMatrices.set( instancedMesh, data ); + + } + + return data.node; + +} + +/** + * TSL object representing a varying property for the instanced color vector. + * + * @type {VaryingNode} + */ +export const instanceColor = /*@__PURE__*/ varyingProperty( 'vec3', 'vInstanceColor' ); + +/** + * TSL function representing the standard instancing vertex shader setup. + * Transforms positionLocal and normalLocal, and assigns varying color in-place. + * + * @tsl + * @function + * @param {number} count - The instance count. + * @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} matrices - The instanced transformation matrices. + * @param {?InstancedBufferAttribute|StorageInstancedBufferAttribute} [colors=null] - The optional instanced colors. + */ +export const instance = /*@__PURE__*/ Fn( ( [ count, matrices, colors = null ], builder ) => { + + // get numeric value (non-node) + count = count.value; + + const isStorageMatrix = matrices.isStorageInstancedBufferAttribute === true; + const isStorageColor = colors && colors.isStorageInstancedBufferAttribute === true; + + const instanceMatrixNode = createInstanceMatrixNode( builder, matrices, count ); + + // interleaved buffer tracking for matrix + let interleavedMatrix = null; + + if ( ! isStorageMatrix ) { + + const uniformBufferSize = count * 16 * 4; + + if ( uniformBufferSize > builder.getUniformBufferLimit() ) { + + interleavedMatrix = _matrixBuffers.get( matrices ); + + } + + } + + let instanceColorNode = null; + let interleavedColor = null; + + if ( colors ) { + + if ( isStorageColor ) { + + instanceColorNode = storage( colors, 'vec3', Math.max( colors.count, 1 ) ).element( instanceIndex ); + + } else { + + let bufferAttribute = _colorBuffers.get( colors ); + + if ( ! bufferAttribute ) { + + bufferAttribute = new InstancedBufferAttribute( colors.array, 3 ); + _colorBuffers.set( colors, bufferAttribute ); + + } + + interleavedColor = bufferAttribute; + + const bufferFn = colors.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute; + + instanceColorNode = vec3( bufferFn( bufferAttribute, 'vec3', 3, 0 ) ); + + } + + } + + // Synchronization of dynamic buffer updates per frame + if ( interleavedMatrix !== null || interleavedColor !== null ) { + + OnFrameUpdate( () => { + + if ( interleavedMatrix !== null ) { + + interleavedMatrix.clearUpdateRanges(); + interleavedMatrix.updateRanges.push( ...matrices.updateRanges ); + + if ( matrices.version !== interleavedMatrix.version ) { + + interleavedMatrix.version = matrices.version; + + } + + } + + if ( colors && interleavedColor !== null ) { + + interleavedColor.clearUpdateRanges(); + interleavedColor.updateRanges.push( ...colors.updateRanges ); + + if ( colors.version !== interleavedColor.version ) { + + interleavedColor.version = colors.version; + + } + + } + + } ); + + } + + // POSITION + + const instancePosition = instanceMatrixNode.mul( positionLocal ).xyz; + positionLocal.assign( instancePosition ); + + if ( builder.needsPreviousData() ) { + + const instancedMesh = builder.object; + + OnObjectUpdate( ( { object } ) => { + + const previousInstanceData = _previousInstanceMatrices.get( object ); + + previousInstanceData.previousInstanceMatrix.array.set( matrices.array ); + + } ); + + const previousInstanceMatrixNode = getPreviousInstance( instancedMesh, matrices, builder, count ); + positionPrevious.assign( previousInstanceMatrixNode.mul( positionPrevious ).xyz ); + + } + + // NORMAL + + if ( builder.hasGeometryAttribute( 'normal' ) ) { + + const instanceNormal = transformNormal( normalLocal, instanceMatrixNode ); + normalLocal.assign( instanceNormal ); + + } + + // COLOR + + if ( instanceColorNode !== null ) { + + instanceColor.assign( instanceColorNode ); + + } + +}, 'void' ); + +/** + * TSL wrapper for applying instanced mesh rendering setup. + * + * @tsl + * @function + * @param {InstancedMesh} instancedMesh - The instanced mesh. + */ +export const instancedMesh = /*@__PURE__*/ Fn( ( [ instancedMesh ] ) => { + + const { count, instanceMatrix, instanceColor } = instancedMesh; + + instance( count, instanceMatrix, instanceColor ); + +}, 'void' ); + + diff --git a/src/nodes/accessors/InstanceNode.js b/src/nodes/accessors/InstanceNode.js deleted file mode 100644 index 716bc01da4cd26..00000000000000 --- a/src/nodes/accessors/InstanceNode.js +++ /dev/null @@ -1,367 +0,0 @@ -import Node from '../core/Node.js'; -import { varyingProperty } from '../core/PropertyNode.js'; -import { instancedBufferAttribute, instancedDynamicBufferAttribute } from './BufferAttributeNode.js'; -import { normalLocal, transformNormal } from './Normal.js'; -import { positionLocal, positionPrevious } from './Position.js'; -import { nodeProxy, vec3, mat4 } from '../tsl/TSLBase.js'; -import { NodeUpdateType } from '../core/constants.js'; -import { buffer } from '../accessors/BufferNode.js'; -import { storage } from './StorageBufferNode.js'; -import { instanceIndex } from '../core/IndexNode.js'; - -import { InstancedInterleavedBuffer } from '../../core/InstancedInterleavedBuffer.js'; -import { InstancedBufferAttribute } from '../../core/InstancedBufferAttribute.js'; -import { DynamicDrawUsage } from '../../constants.js'; - -/** - * This node implements the vertex shader logic which is required - * when rendering 3D objects via instancing. The code makes sure - * vertex positions, normals and colors can be modified via instanced - * data. - * - * @augments Node - */ -class InstanceNode extends Node { - - static get type() { - - return 'InstanceNode'; - - } - - /** - * Constructs a new instance node. - * - * @param {number} count - The number of instances. - * @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceMatrix - Instanced buffer attribute representing the instance transformations. - * @param {?InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceColor - Instanced buffer attribute representing the instance colors. - */ - constructor( count, instanceMatrix, instanceColor = null ) { - - super( 'void' ); - - /** - * The number of instances. - * - * @type {number} - */ - this.count = count; - - /** - * Instanced buffer attribute representing the transformation of instances. - * - * @type {InstancedBufferAttribute} - */ - this.instanceMatrix = instanceMatrix; - - /** - * Instanced buffer attribute representing the color of instances. - * - * @type {InstancedBufferAttribute} - */ - this.instanceColor = instanceColor; - - /** - * The node that represents the instance matrix data. - * - * @type {?Node} - */ - this.instanceMatrixNode = null; - - /** - * The node that represents the instance color data. - * - * @type {?Node} - * @default null - */ - this.instanceColorNode = null; - - /** - * The update type is set to `frame` for updating - * velocity-related data. - * - * @type {string} - * @default 'frame' - */ - this.updateType = NodeUpdateType.FRAME; - - /** - * The update type is set to `frame` since an update - * of instanced buffer data must be checked per frame. - * - * @type {string} - * @default 'frame' - */ - this.updateBeforeType = NodeUpdateType.FRAME; - - /** - * A reference to a buffer that is used by `instanceMatrixNode`. - * - * @type {?InstancedInterleavedBuffer} - */ - this.buffer = null; - - /** - * A reference to a buffer that is used by `instanceColorNode`. - * - * @type {?InstancedBufferAttribute} - */ - this.bufferColor = null; - - /** - * The previous instance matrices. Required for computing motion vectors. - * - * @type {?Node} - * @default null - */ - this.previousInstanceMatrixNode = null; - - } - - /** - * Tracks whether the matrix data is provided via a storage buffer. - * - * @type {boolean} - */ - get isStorageMatrix() { - - const { instanceMatrix } = this; - - return instanceMatrix && instanceMatrix.isStorageInstancedBufferAttribute === true; - - } - - /** - * Tracks whether the color data is provided via a storage buffer. - * - * @type {boolean} - */ - get isStorageColor() { - - const { instanceColor } = this; - - return instanceColor && instanceColor.isStorageInstancedBufferAttribute === true; - - } - - /** - * Setups the internal buffers and nodes and assigns the transformed vertex data - * to predefined node variables for accumulation. That follows the same patterns - * like with morph and skinning nodes. - * - * @param {NodeBuilder} builder - The current node builder. - */ - setup( builder ) { - - let { instanceMatrixNode, instanceColorNode } = this; - - // instance matrix - - if ( instanceMatrixNode === null ) { - - instanceMatrixNode = this._createInstanceMatrixNode( true, builder ); - - this.instanceMatrixNode = instanceMatrixNode; - - } - - // instance color - - const { instanceColor, isStorageColor } = this; - - if ( instanceColor && instanceColorNode === null ) { - - if ( isStorageColor ) { - - instanceColorNode = storage( instanceColor, 'vec3', Math.max( instanceColor.count, 1 ) ).element( instanceIndex ); - - } else { - - const bufferAttribute = new InstancedBufferAttribute( instanceColor.array, 3 ); - - const bufferFn = instanceColor.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute; - - this.bufferColor = bufferAttribute; - - instanceColorNode = vec3( bufferFn( bufferAttribute, 'vec3', 3, 0 ) ); - - } - - this.instanceColorNode = instanceColorNode; - - } - - // POSITION - - const instancePosition = instanceMatrixNode.mul( positionLocal ).xyz; - positionLocal.assign( instancePosition ); - - if ( builder.needsPreviousData() ) { - - positionPrevious.assign( this.getPreviousInstancedPosition( builder ) ); - - } - - // NORMAL - - if ( builder.hasGeometryAttribute( 'normal' ) ) { - - const instanceNormal = transformNormal( normalLocal, instanceMatrixNode ); - - // ASSIGNS - - normalLocal.assign( instanceNormal ); - - } - - // COLOR - - if ( this.instanceColorNode !== null ) { - - varyingProperty( 'vec3', 'vInstanceColor' ).assign( this.instanceColorNode ); - - } - - } - - /** - * Checks if the internal buffers require an update. - * - * @param {NodeFrame} frame - The current node frame. - */ - updateBefore( /*frame*/ ) { - - if ( this.buffer !== null && this.isStorageMatrix !== true ) { - - this.buffer.clearUpdateRanges(); - this.buffer.updateRanges.push( ... this.instanceMatrix.updateRanges ); - - // update version if necessary - - if ( this.instanceMatrix.version !== this.buffer.version ) { - - this.buffer.version = this.instanceMatrix.version; - - } - - } - - if ( this.instanceColor && this.bufferColor !== null && this.isStorageColor !== true ) { - - this.bufferColor.clearUpdateRanges(); - this.bufferColor.updateRanges.push( ... this.instanceColor.updateRanges ); - - if ( this.instanceColor.version !== this.bufferColor.version ) { - - this.bufferColor.version = this.instanceColor.version; - - } - - } - - } - - /** - * Updates velocity-related data if necessary. - * - * @param {NodeFrame} frame - The current node frame. - */ - update( frame ) { - - if ( this.previousInstanceMatrixNode !== null ) { - - frame.object.previousInstanceMatrix.array.set( this.instanceMatrix.array ); - - } - - } - - /** - * Computes the transformed/instanced vertex position of the previous frame. - * - * @param {NodeBuilder} builder - The current node builder. - * @return {Node} The instanced position from the previous frame. - */ - getPreviousInstancedPosition( builder ) { - - const instancedMesh = builder.object; - - if ( this.previousInstanceMatrixNode === null ) { - - instancedMesh.previousInstanceMatrix = this.instanceMatrix.clone(); - - this.previousInstanceMatrixNode = this._createInstanceMatrixNode( false, builder ); - - } - - return this.previousInstanceMatrixNode.mul( positionPrevious ).xyz; - - } - - /** - * Creates a node representing the instance matrix data. - * - * @private - * @param {boolean} assignBuffer - Whether the created interleaved buffer should be assigned to the `buffer` member or not. - * @param {NodeBuilder} builder - A reference to the current node builder. - * @return {Node} The instance matrix node. - */ - _createInstanceMatrixNode( assignBuffer, builder ) { - - let instanceMatrixNode; - - const { instanceMatrix } = this; - const { count } = instanceMatrix; - - if ( this.isStorageMatrix ) { - - instanceMatrixNode = storage( instanceMatrix, 'mat4', Math.max( count, 1 ) ).element( instanceIndex ); - - } else { - - const uniformBufferSize = count * 16 * 4; // count * 16 components * 4 bytes (float) - - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { - - instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', Math.max( count, 1 ) ).element( instanceIndex ); - - } else { - - const interleaved = new InstancedInterleavedBuffer( instanceMatrix.array, 16, 1 ); - - if ( assignBuffer === true ) this.buffer = interleaved; - - const bufferFn = instanceMatrix.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute; - - const instanceBuffers = [ - bufferFn( interleaved, 'vec4', 16, 0 ), - bufferFn( interleaved, 'vec4', 16, 4 ), - bufferFn( interleaved, 'vec4', 16, 8 ), - bufferFn( interleaved, 'vec4', 16, 12 ) - ]; - - instanceMatrixNode = mat4( ...instanceBuffers ); - - } - - } - - return instanceMatrixNode; - - } - -} - -export default InstanceNode; - -/** - * TSL function for creating an instance node. - * - * @tsl - * @function - * @param {number} count - The number of instances. - * @param {InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceMatrix - Instanced buffer attribute representing the instance transformations. - * @param {?InstancedBufferAttribute|StorageInstancedBufferAttribute} instanceColor - Instanced buffer attribute representing the instance colors. - * @returns {InstanceNode} - */ -export const instance = /*@__PURE__*/ nodeProxy( InstanceNode ).setParameterLength( 2, 3 ); diff --git a/src/nodes/accessors/InstancedMeshNode.js b/src/nodes/accessors/InstancedMeshNode.js deleted file mode 100644 index d5542ea4059938..00000000000000 --- a/src/nodes/accessors/InstancedMeshNode.js +++ /dev/null @@ -1,50 +0,0 @@ -import InstanceNode from './InstanceNode.js'; -import { nodeProxy } from '../tsl/TSLBase.js'; - -/** - * This is a special version of `InstanceNode` which requires the usage of {@link InstancedMesh}. - * It allows an easier setup of the instance node. - * - * @augments InstanceNode - */ -class InstancedMeshNode extends InstanceNode { - - static get type() { - - return 'InstancedMeshNode'; - - } - - /** - * Constructs a new instanced mesh node. - * - * @param {InstancedMesh} instancedMesh - The instanced mesh. - */ - constructor( instancedMesh ) { - - const { count, instanceMatrix, instanceColor } = instancedMesh; - - super( count, instanceMatrix, instanceColor ); - - /** - * A reference to the instanced mesh. - * - * @type {InstancedMesh} - */ - this.instancedMesh = instancedMesh; - - } - -} - -export default InstancedMeshNode; - -/** - * TSL function for creating an instanced mesh node. - * - * @tsl - * @function - * @param {InstancedMesh} instancedMesh - The instancedMesh. - * @returns {InstancedMeshNode} - */ -export const instancedMesh = /*@__PURE__*/ nodeProxy( InstancedMeshNode ).setParameterLength( 1 ); diff --git a/src/nodes/accessors/MorphNode.js b/src/nodes/accessors/Morph.js similarity index 56% rename from src/nodes/accessors/MorphNode.js rename to src/nodes/accessors/Morph.js index c05620b3ef66d8..361dd79b2db7b5 100644 --- a/src/nodes/accessors/MorphNode.js +++ b/src/nodes/accessors/Morph.js @@ -1,13 +1,12 @@ -import Node from '../core/Node.js'; -import { NodeUpdateType } from '../core/constants.js'; -import { float, nodeProxy, Fn, ivec2, int, If } from '../tsl/TSLBase.js'; -import { uniform } from '../core/UniformNode.js'; + +import { float, Fn, ivec2, int, If, uniform } from '../tsl/TSLBase.js'; import { reference } from './ReferenceNode.js'; +import { Loop } from '../utils/LoopNode.js'; +import { OnObjectUpdate } from '../utils/EventNode.js'; +import { textureLoad } from './TextureNode.js'; import { positionLocal } from './Position.js'; import { normalLocal } from './Normal.js'; -import { textureLoad } from './TextureNode.js'; import { instanceIndex, vertexIndex } from '../core/IndexNode.js'; -import { Loop } from '../utils/LoopNode.js'; import { DataArrayTexture } from '../../textures/DataArrayTexture.js'; import { Vector2 } from '../../math/Vector2.js'; @@ -16,7 +15,20 @@ import { FloatType } from '../../constants.js'; const _morphTextures = /*@__PURE__*/ new WeakMap(); const _morphVec4 = /*@__PURE__*/ new Vector4(); +const _morphBaseInfluences = /*@__PURE__*/ new WeakMap(); +/** + * TSL function that retrieves and scales the morphed attribute (position or normal) texel value. + * + * @param {Object} params - The parameter object. + * @param {Node} params.bufferMap - The morph target data array texture. + * @param {Node} params.influence - The target's animation influence weight. + * @param {number} params.stride - The vertex data stride (e.g. 1 or 2). + * @param {Node} params.width - The texture width limit. + * @param {Node} params.depth - The target layer index (morph target index). + * @param {Node} params.offset - The texture offset (e.g. 0 for position, 1 for normal). + * @returns {Node} The scaled morph target translation value. + */ const getMorph = /*@__PURE__*/ Fn( ( { bufferMap, influence, stride, width, depth, offset } ) => { const texelIndex = int( vertexIndex ).mul( stride ).add( offset ); @@ -30,6 +42,12 @@ const getMorph = /*@__PURE__*/ Fn( ( { bufferMap, influence, stride, width, dept } ); +/** + * Resolves or creates a compiled DataArrayTexture containing encoded vertex morph targets data for WebGL2/WebGPU. + * + * @param {BufferGeometry} geometry - The geometry to parse. + * @returns {Object} The resolved morph targets texture data mapping entry. + */ function getEntry( geometry ) { const hasMorphPosition = geometry.morphAttributes.position !== undefined; @@ -157,154 +175,108 @@ function getEntry( geometry ) { } /** - * This node implements the vertex transformation shader logic which is required - * for morph target animation. + * TSL object representing a reference to the mesh's morphTargetInfluences array. * - * @augments Node + * @type {ReferenceNode} */ -class MorphNode extends Node { - - static get type() { - - return 'MorphNode'; +export const morphTargetInfluences = /*@__PURE__*/ reference( 'morphTargetInfluences', 'float' ); - } - - /** - * Constructs a new morph node. - * - * @param {Mesh} mesh - The mesh holding the morph targets. - */ - constructor( mesh ) { - - super( 'void' ); - - /** - * The mesh holding the morph targets. - * - * @type {Mesh} - */ - this.mesh = mesh; - - /** - * A uniform node which represents the morph base influence value. - * - * @type {UniformNode} - */ - this.morphBaseInfluence = uniform( 1 ); - - /** - * The update type overwritten since morph nodes are updated per object. - * - * @type {string} - */ - this.updateType = NodeUpdateType.OBJECT; +/** + * TSL function representing the vertex shader morph targets blend setup. + * Dynamically computes morph targets weights and updates positionLocal and normalLocal in-place. + * + * @tsl + * @function + * @param {Mesh} mesh - The mesh. + */ +export const morphReference = /*@__PURE__*/ Fn( ( [ mesh ] ) => { - } + const { geometry } = mesh; - /** - * Setups the morph node by assigning the transformed vertex data to predefined node variables. - * - * @param {NodeBuilder} builder - The current node builder. - */ - setup( builder ) { + const hasMorphPosition = geometry.morphAttributes.position !== undefined; + const hasMorphNormals = geometry.hasAttribute( 'normal' ) && geometry.morphAttributes.normal !== undefined; - const { geometry } = builder; + const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color; + const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0; - const hasMorphPosition = geometry.morphAttributes.position !== undefined; - const hasMorphNormals = geometry.hasAttribute( 'normal' ) && geometry.morphAttributes.normal !== undefined; + if ( morphTargetsCount === 0 ) return; - const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color; - const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0; + let morphBaseInfluence = _morphBaseInfluences.get( mesh ); - // nodes + if ( ! morphBaseInfluence ) { - const { texture: bufferMap, stride, size } = getEntry( geometry ); + morphBaseInfluence = uniform( 1 ); + _morphBaseInfluences.set( mesh, morphBaseInfluence ); - if ( hasMorphPosition === true ) positionLocal.mulAssign( this.morphBaseInfluence ); - if ( hasMorphNormals === true ) normalLocal.mulAssign( this.morphBaseInfluence ); + OnObjectUpdate( ( { object } ) => { - const width = int( size.width ); + if ( object.geometry.morphTargetsRelative ) { - Loop( morphTargetsCount, ( { i } ) => { + morphBaseInfluence.value = 1; - const influence = float( 0 ).toVar(); + } else { - if ( this.mesh.count > 1 && ( this.mesh.morphTexture !== null && this.mesh.morphTexture !== undefined ) ) { + morphBaseInfluence.value = 1 - object.morphTargetInfluences.reduce( ( a, b ) => a + b, 0 ); - influence.assign( textureLoad( this.mesh.morphTexture, ivec2( int( i ).add( 1 ), int( instanceIndex ) ) ).r ); + } - } else { + } ); - influence.assign( reference( 'morphTargetInfluences', 'float' ).element( i ).toVar() ); + } - } + const { texture: bufferMap, stride, size } = getEntry( geometry ); - If( influence.notEqual( 0 ), () => { + if ( hasMorphPosition === true ) positionLocal.mulAssign( morphBaseInfluence ); + if ( hasMorphNormals === true ) normalLocal.mulAssign( morphBaseInfluence ); - if ( hasMorphPosition === true ) { + const width = int( size.width ); - positionLocal.addAssign( getMorph( { - bufferMap, - influence, - stride, - width, - depth: i, - offset: int( 0 ) - } ) ); + Loop( morphTargetsCount, ( { i } ) => { - } + const influence = float( 0 ).toVar(); - if ( hasMorphNormals === true ) { + if ( mesh.count > 1 && ( mesh.morphTexture !== null && mesh.morphTexture !== undefined ) ) { - normalLocal.addAssign( getMorph( { - bufferMap, - influence, - stride, - width, - depth: i, - offset: int( 1 ) - } ) ); + influence.assign( textureLoad( mesh.morphTexture, ivec2( int( i ).add( 1 ), int( instanceIndex ) ) ).r ); - } + } else { - } ); + influence.assign( morphTargetInfluences.element( i ).toVar() ); - } ); + } - } + If( influence.notEqual( 0 ), () => { - /** - * Updates the state of the morphed mesh by updating the base influence. - * - * @param {NodeFrame} frame - The current node frame. - */ - update( /*frame*/ ) { + if ( hasMorphPosition === true ) { - const morphBaseInfluence = this.morphBaseInfluence; + positionLocal.addAssign( getMorph( { + bufferMap, + influence, + stride, + width, + depth: i, + offset: int( 0 ) + } ) ); - if ( this.mesh.geometry.morphTargetsRelative ) { + } - morphBaseInfluence.value = 1; + if ( hasMorphNormals === true ) { - } else { + normalLocal.addAssign( getMorph( { + bufferMap, + influence, + stride, + width, + depth: i, + offset: int( 1 ) + } ) ); - morphBaseInfluence.value = 1 - this.mesh.morphTargetInfluences.reduce( ( a, b ) => a + b, 0 ); + } - } + } ); - } + } ); -} +}, 'void' ); -export default MorphNode; -/** - * TSL function for creating a morph node. - * - * @tsl - * @function - * @param {Mesh} mesh - The mesh holding the morph targets. - * @returns {MorphNode} - */ -export const morphReference = /*@__PURE__*/ nodeProxy( MorphNode ).setParameterLength( 1 ); diff --git a/src/nodes/accessors/Skinning.js b/src/nodes/accessors/Skinning.js new file mode 100644 index 00000000000000..2356aed8656dce --- /dev/null +++ b/src/nodes/accessors/Skinning.js @@ -0,0 +1,263 @@ + +import { Fn, add, uniform } from '../tsl/TSLBase.js'; +import { attribute } from '../core/AttributeNode.js'; +import { OnObjectUpdate } from '../utils/EventNode.js'; +import { normalLocal } from './Normal.js'; +import { positionLocal, positionPrevious } from './Position.js'; +import { tangentLocal } from './Tangent.js'; +import { reference, referenceBuffer } from './ReferenceNode.js'; +import { buffer } from './BufferNode.js'; +import { storage } from './StorageBufferNode.js'; +import { instanceIndex } from '../core/IndexNode.js'; + +import { InstancedBufferAttribute } from '../../core/InstancedBufferAttribute.js'; + +const _skeletonsUpdated = /*@__PURE__*/ new WeakMap(); +const _previousBoneMatricesData = /*@__PURE__*/ new WeakMap(); + +/** + * Computes the skinned position by applying bone matrices based on weights. + * + * @param {Node} boneMatrices - The bone matrices buffer or storage node. + * @param {Node} position - The vertex position to transform. + * @param {Node} bindMatrix - The bind matrix node. + * @param {Node} bindMatrixInverse - The inverse bind matrix node. + * @param {Node} skinIndex - The skin index attribute. + * @param {Node} skinWeight - The skin weight attribute. + * @returns {Node} The skinned position. + */ +function getSkinnedPosition( boneMatrices, position, bindMatrix, bindMatrixInverse, skinIndex, skinWeight ) { + + const boneMatX = boneMatrices.element( skinIndex.x ); + const boneMatY = boneMatrices.element( skinIndex.y ); + const boneMatZ = boneMatrices.element( skinIndex.z ); + const boneMatW = boneMatrices.element( skinIndex.w ); + + // POSITION + + const skinVertex = bindMatrix.mul( position ); + + const skinned = add( + boneMatX.mul( skinWeight.x ).mul( skinVertex ), + boneMatY.mul( skinWeight.y ).mul( skinVertex ), + boneMatZ.mul( skinWeight.z ).mul( skinVertex ), + boneMatW.mul( skinWeight.w ).mul( skinVertex ) + ); + + return bindMatrixInverse.mul( skinned ).xyz; + +} + +/** + * Computes the skinned normal and tangent vectors by applying bone matrices based on weights. + * + * @param {Node} boneMatrices - The bone matrices buffer or storage node. + * @param {Node} normal - The normal vector in local space. + * @param {Node} tangent - The tangent vector in local space. + * @param {Node} bindMatrix - The bind matrix node. + * @param {Node} bindMatrixInverse - The inverse bind matrix node. + * @param {Node} skinIndex - The skin index attribute. + * @param {Node} skinWeight - The skin weight attribute. + * @returns {{skinNormal: Node, skinTangent: Node}} The skinned normal and tangent. + */ +function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix, bindMatrixInverse, skinIndex, skinWeight ) { + + const boneMatX = boneMatrices.element( skinIndex.x ); + const boneMatY = boneMatrices.element( skinIndex.y ); + const boneMatZ = boneMatrices.element( skinIndex.z ); + const boneMatW = boneMatrices.element( skinIndex.w ); + + // NORMAL and TANGENT + + let skinMatrix = add( + skinWeight.x.mul( boneMatX ), + skinWeight.y.mul( boneMatY ), + skinWeight.z.mul( boneMatZ ), + skinWeight.w.mul( boneMatW ) + ); + + skinMatrix = bindMatrixInverse.mul( skinMatrix ).mul( bindMatrix ); + + const skinNormal = skinMatrix.transformDirection( normal ).xyz; + const skinTangent = skinMatrix.transformDirection( tangent ).xyz; + + return { skinNormal, skinTangent }; + +} + +/** + * Retrieves or initializes the previous frame skinned position node for motion vectors. + * Uses a WeakMap to cache previous frame bone matrix arrays and their TSL buffer nodes. + * + * @param {SkinnedMesh} skinnedMesh - The skinned mesh. + * @param {Node} bindMatrixNode - The bind matrix node. + * @param {Node} bindMatrixInverseNode - The inverse bind matrix node. + * @param {Node} skinIndexNode - The skin index attribute. + * @param {Node} skinWeightNode - The skin weight attribute. + * @returns {Node} The skinned position from the previous frame. + */ +function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ) { + + const skeleton = skinnedMesh.skeleton; + + let data = _previousBoneMatricesData.get( skeleton ); + + if ( data === undefined ) { + + skeleton.update(); + + const previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); + + data = { + previousBoneMatrices, + node: buffer( previousBoneMatrices, 'mat4', skeleton.bones.length ) + }; + + _previousBoneMatricesData.set( skeleton, data ); + + } + + return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + +} + +/** + * TSL function representing the standard skeletal animation vertex shader setup. + * Transforms positionLocal, normalLocal, and tangentLocal in-place. + * + * @tsl + * @function + * @param {SkinnedMesh} skinnedMesh - The skinned mesh. + */ +export const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { + + const skinIndexNode = attribute( 'skinIndex', 'uvec4' ); + const skinWeightNode = attribute( 'skinWeight', 'vec4' ); + const bindMatrixNode = reference( 'bindMatrix', 'mat4' ); + const bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' ); + const boneMatricesNode = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skinnedMesh.skeleton.bones.length ); + + OnObjectUpdate( ( { object, frameId } ) => { + + const skeleton = object.skeleton; + + if ( _skeletonsUpdated.get( skeleton ) !== frameId ) { + + _skeletonsUpdated.set( skeleton, frameId ); + + const skeletonData = _previousBoneMatricesData.get( skeleton ); + + if ( skeletonData !== undefined ) { + + skeletonData.previousBoneMatrices.set( skeleton.boneMatrices ); + + } + + skeleton.update(); + + } + + } ); + + if ( builder.needsPreviousData() ) { + + const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + + positionPrevious.assign( previousSkinnedPosition ); + + } + + const skinPosition = getSkinnedPosition( boneMatricesNode, positionLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + positionLocal.assign( skinPosition ); + + if ( builder.hasGeometryAttribute( 'normal' ) ) { + + const { skinNormal, skinTangent } = getSkinnedNormalAndTangent( boneMatricesNode, normalLocal, tangentLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + + normalLocal.assign( skinNormal ); + + if ( builder.hasGeometryAttribute( 'tangent' ) ) { + + tangentLocal.assign( skinTangent ); + + } + + } + +}, 'void' ); + +/** + * TSL function that computes skeletal animation for custom compute passes. + * + * @tsl + * @function + * @param {SkinnedMesh} skinnedMesh - The skinned mesh. + * @param {Node} [toPosition=null] - The target position node to assign. + * @returns {Node} The computed skinned position node. + */ +export const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], builder ) => { + + const positionNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'position' ).array, 3 ), 'vec3' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar(); + const skinIndexNode = storage( new InstancedBufferAttribute( new Uint32Array( skinnedMesh.geometry.getAttribute( 'skinIndex' ).array ), 4 ), 'uvec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar(); + const skinWeightNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'skinWeight' ).array, 4 ), 'vec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar(); + const bindMatrixNode = uniform( skinnedMesh.bindMatrix, 'mat4' ); + const bindMatrixInverseNode = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ); + const boneMatricesNode = buffer( skinnedMesh.skeleton.boneMatrices, 'mat4', skinnedMesh.skeleton.bones.length ); + + const skeleton = skinnedMesh.skeleton; + + OnObjectUpdate( ( { frameId } ) => { + + if ( _skeletonsUpdated.get( skeleton ) !== frameId ) { + + _skeletonsUpdated.set( skeleton, frameId ); + + const state = _previousBoneMatricesData.get( skeleton ); + + if ( state !== undefined ) { + + state.previousBoneMatrices.set( skeleton.boneMatrices ); + + } + + skeleton.update(); + + } + + } ); + + if ( builder.needsPreviousData() ) { + + const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + + positionPrevious.assign( previousSkinnedPosition ); + + } + + const skinPosition = getSkinnedPosition( boneMatricesNode, positionNode, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + + if ( toPosition !== null ) { + + toPosition.assign( skinPosition ); + + } + + if ( builder.hasGeometryAttribute( 'normal' ) ) { + + const { skinNormal, skinTangent } = getSkinnedNormalAndTangent( boneMatricesNode, normalLocal, tangentLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + + normalLocal.assign( skinNormal ); + + if ( builder.hasGeometryAttribute( 'tangent' ) ) { + + tangentLocal.assign( skinTangent ); + + } + + } + + return skinPosition; + +} ); + + diff --git a/src/nodes/accessors/SkinningNode.js b/src/nodes/accessors/SkinningNode.js deleted file mode 100644 index 589a2270608d22..00000000000000 --- a/src/nodes/accessors/SkinningNode.js +++ /dev/null @@ -1,328 +0,0 @@ -import Node from '../core/Node.js'; -import { NodeUpdateType } from '../core/constants.js'; -import { nodeObject } from '../tsl/TSLBase.js'; -import { attribute } from '../core/AttributeNode.js'; -import { reference, referenceBuffer } from './ReferenceNode.js'; -import { add } from '../math/OperatorNode.js'; -import { normalLocal } from './Normal.js'; -import { positionLocal, positionPrevious } from './Position.js'; -import { tangentLocal } from './Tangent.js'; -import { uniform } from '../core/UniformNode.js'; -import { buffer } from './BufferNode.js'; -import { storage } from './StorageBufferNode.js'; -import { InstancedBufferAttribute } from '../../core/InstancedBufferAttribute.js'; -import { instanceIndex } from '../core/IndexNode.js'; - -const _frameId = new WeakMap(); - -/** - * This node implements the vertex transformation shader logic which is required - * for skinning/skeletal animation. - * - * @augments Node - */ -class SkinningNode extends Node { - - static get type() { - - return 'SkinningNode'; - - } - - /** - * Constructs a new skinning node. - * - * @param {SkinnedMesh} skinnedMesh - The skinned mesh. - */ - constructor( skinnedMesh ) { - - super( 'void' ); - - /** - * The skinned mesh. - * - * @type {SkinnedMesh} - */ - this.skinnedMesh = skinnedMesh; - - /** - * The update type overwritten since skinning nodes are updated per object. - * - * @type {string} - */ - this.updateType = NodeUpdateType.OBJECT; - - // - - /** - * The skin index attribute. - * - * @type {AttributeNode} - */ - this.skinIndexNode = attribute( 'skinIndex', 'uvec4' ); - - /** - * The skin weight attribute. - * - * @type {AttributeNode} - */ - this.skinWeightNode = attribute( 'skinWeight', 'vec4' ); - - /** - * The bind matrix node. - * - * @type {Node} - */ - this.bindMatrixNode = reference( 'bindMatrix', 'mat4' ); - - /** - * The bind matrix inverse node. - * - * @type {Node} - */ - this.bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' ); - - /** - * The bind matrices as a uniform buffer node. - * - * @type {Node} - */ - this.boneMatricesNode = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skinnedMesh.skeleton.bones.length ); - - /** - * The current vertex position in local space. - * - * @type {Node} - */ - this.positionNode = positionLocal; - - /** - * The result of vertex position in local space. - * - * @type {Node} - */ - this.toPositionNode = positionLocal; - - /** - * The previous bind matrices as a uniform buffer node. - * Required for computing motion vectors. - * - * @type {?Node} - * @default null - */ - this.previousBoneMatricesNode = null; - - } - - /** - * Transforms the given vertex position via skinning. - * - * @param {Node} [boneMatrices=this.boneMatricesNode] - The bone matrices - * @param {Node} [position=this.positionNode] - The vertex position in local space. - * @return {Node} The transformed vertex position. - */ - getSkinnedPosition( boneMatrices = this.boneMatricesNode, position = this.positionNode ) { - - const { skinIndexNode, skinWeightNode, bindMatrixNode, bindMatrixInverseNode } = this; - - const boneMatX = boneMatrices.element( skinIndexNode.x ); - const boneMatY = boneMatrices.element( skinIndexNode.y ); - const boneMatZ = boneMatrices.element( skinIndexNode.z ); - const boneMatW = boneMatrices.element( skinIndexNode.w ); - - // POSITION - - const skinVertex = bindMatrixNode.mul( position ); - - const skinned = add( - boneMatX.mul( skinWeightNode.x ).mul( skinVertex ), - boneMatY.mul( skinWeightNode.y ).mul( skinVertex ), - boneMatZ.mul( skinWeightNode.z ).mul( skinVertex ), - boneMatW.mul( skinWeightNode.w ).mul( skinVertex ) - ); - - return bindMatrixInverseNode.mul( skinned ).xyz; - - } - - /** - * Transforms the given vertex normal and tangent via skinning. - * - * @param {Node} [boneMatrices=this.boneMatricesNode] - The bone matrices - * @param {Node} [normal=normalLocal] - The vertex normal in local space. - * @param {Node} [tangent=tangentLocal] - The vertex tangent in local space. - * @return {{skinNormal: Node, skinTangent:Node}} The transformed vertex normal and tangent. - */ - getSkinnedNormalAndTangent( boneMatrices = this.boneMatricesNode, normal = normalLocal, tangent = tangentLocal ) { - - const { skinIndexNode, skinWeightNode, bindMatrixNode, bindMatrixInverseNode } = this; - - const boneMatX = boneMatrices.element( skinIndexNode.x ); - const boneMatY = boneMatrices.element( skinIndexNode.y ); - const boneMatZ = boneMatrices.element( skinIndexNode.z ); - const boneMatW = boneMatrices.element( skinIndexNode.w ); - - // NORMAL and TANGENT - - let skinMatrix = add( - skinWeightNode.x.mul( boneMatX ), - skinWeightNode.y.mul( boneMatY ), - skinWeightNode.z.mul( boneMatZ ), - skinWeightNode.w.mul( boneMatW ) - ); - - skinMatrix = bindMatrixInverseNode.mul( skinMatrix ).mul( bindMatrixNode ); - - const skinNormal = skinMatrix.transformDirection( normal ).xyz; - const skinTangent = skinMatrix.transformDirection( tangent ).xyz; - - return { skinNormal, skinTangent }; - - } - - /** - * Computes the transformed/skinned vertex position of the previous frame. - * - * @param {NodeBuilder} builder - The current node builder. - * @return {Node} The skinned position from the previous frame. - */ - getPreviousSkinnedPosition( builder ) { - - const skinnedMesh = builder.object; - - if ( this.previousBoneMatricesNode === null ) { - - skinnedMesh.skeleton.previousBoneMatrices = new Float32Array( skinnedMesh.skeleton.boneMatrices ); - - this.previousBoneMatricesNode = referenceBuffer( 'skeleton.previousBoneMatrices', 'mat4', skinnedMesh.skeleton.bones.length ); - - } - - return this.getSkinnedPosition( this.previousBoneMatricesNode, positionPrevious ); - - } - - /** - * Setups the skinning node by assigning the transformed vertex data to predefined node variables. - * - * @param {NodeBuilder} builder - The current node builder. - * @return {Node} The transformed vertex position. - */ - setup( builder ) { - - if ( builder.needsPreviousData() ) { - - positionPrevious.assign( this.getPreviousSkinnedPosition( builder ) ); - - } - - const skinPosition = this.getSkinnedPosition(); - - if ( this.toPositionNode ) this.toPositionNode.assign( skinPosition ); - - // - - if ( builder.hasGeometryAttribute( 'normal' ) ) { - - const { skinNormal, skinTangent } = this.getSkinnedNormalAndTangent(); - - normalLocal.assign( skinNormal ); - - if ( builder.hasGeometryAttribute( 'tangent' ) ) { - - tangentLocal.assign( skinTangent ); - - } - - } - - return skinPosition; - - } - - /** - * Generates the code snippet of the skinning node. - * - * @param {NodeBuilder} builder - The current node builder. - * @param {string} output - The current output. - * @return {string} The generated code snippet. - */ - generate( builder, output ) { - - if ( output !== 'void' ) { - - return super.generate( builder, output ); - - } - - } - - /** - * Updates the state of the skinned mesh by updating the skeleton once per frame. - * - * @param {NodeFrame} frame - The current node frame. - */ - update( frame ) { - - const skeleton = frame.object && frame.object.skeleton ? frame.object.skeleton : this.skinnedMesh.skeleton; - - if ( _frameId.get( skeleton ) === frame.frameId ) return; - - _frameId.set( skeleton, frame.frameId ); - - if ( this.previousBoneMatricesNode !== null ) { - - if ( skeleton.previousBoneMatrices === null ) { - - // cloned skeletons miss "previousBoneMatrices" in their first updated - - skeleton.previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); - - } - - skeleton.previousBoneMatrices.set( skeleton.boneMatrices ); - - - } - - skeleton.update(); - - } - -} - -export default SkinningNode; - -/** - * TSL function for creating a skinning node. - * - * @tsl - * @function - * @param {SkinnedMesh} skinnedMesh - The skinned mesh. - * @returns {SkinningNode} - */ -export const skinning = ( skinnedMesh ) => new SkinningNode( skinnedMesh ); - -/** - * TSL function for computing skinning. - * - * @tsl - * @function - * @param {SkinnedMesh} skinnedMesh - The skinned mesh. - * @param {Node} [toPosition=null] - The target position. - * @returns {SkinningNode} - */ -export const computeSkinning = ( skinnedMesh, toPosition = null ) => { - - const node = new SkinningNode( skinnedMesh ); - node.positionNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'position' ).array, 3 ), 'vec3' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar(); - node.skinIndexNode = storage( new InstancedBufferAttribute( new Uint32Array( skinnedMesh.geometry.getAttribute( 'skinIndex' ).array ), 4 ), 'uvec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar(); - node.skinWeightNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'skinWeight' ).array, 4 ), 'vec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar(); - node.bindMatrixNode = uniform( skinnedMesh.bindMatrix, 'mat4' ); - node.bindMatrixInverseNode = uniform( skinnedMesh.bindMatrixInverse, 'mat4' ); - node.boneMatricesNode = buffer( skinnedMesh.skeleton.boneMatrices, 'mat4', skinnedMesh.skeleton.bones.length ); - node.toPositionNode = toPosition; - - return nodeObject( node ); - -}; diff --git a/src/objects/InstancedMesh.js b/src/objects/InstancedMesh.js index d519633ddfff5c..7c29cc68873fe3 100644 --- a/src/objects/InstancedMesh.js +++ b/src/objects/InstancedMesh.js @@ -56,15 +56,6 @@ class InstancedMesh extends Mesh { */ this.instanceMatrix = new InstancedBufferAttribute( new Float32Array( count * 16 ), 16 ); - /** - * Represents the local transformation of all instances of the previous frame. - * Required for computing velocity. Maintained in {@link InstanceNode}. - * - * @type {?InstancedBufferAttribute} - * @default null - */ - this.previousInstanceMatrix = null; - /** * Represents the color of all instances. You have to set its * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data @@ -194,8 +185,6 @@ class InstancedMesh extends Mesh { this.instanceMatrix.copy( source.instanceMatrix ); - if ( source.previousInstanceMatrix !== null ) this.previousInstanceMatrix = source.previousInstanceMatrix.clone(); - if ( source.morphTexture !== null ) this.morphTexture = source.morphTexture.clone(); if ( source.instanceColor !== null ) this.instanceColor = source.instanceColor.clone(); diff --git a/src/objects/Skeleton.js b/src/objects/Skeleton.js index 5d1361086ae3c8..31384a9bcd40eb 100644 --- a/src/objects/Skeleton.js +++ b/src/objects/Skeleton.js @@ -70,15 +70,6 @@ class Skeleton { */ this.boneMatrices = null; - /** - * An array buffer holding the bone data of the previous frame. - * Required for computing velocity. Maintained in {@link SkinningNode}. - * - * @type {?Float32Array} - * @default null - */ - this.previousBoneMatrices = null; - /** * A texture holding the bone data for use * in the vertex shader. diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js index 0d732bdeb46d6c..1503ff56e96c92 100644 --- a/src/renderers/common/RenderObject.js +++ b/src/renderers/common/RenderObject.js @@ -684,7 +684,7 @@ class RenderObject { // structural equality isn't sufficient for morph targets since the // data are maintained in textures. only if the targets are all equal - // the texture and thus the instance of `MorphNode` can be shared. + // the texture and thus the `morphReference` can be shared. for ( const name of Object.keys( geometry.morphAttributes ).sort() ) {