three
Version:
JavaScript 3D library
370 lines (239 loc) • 10.4 kB
JavaScript
import { vec3, mat4, Fn } from '../tsl/TSLBase.js';
import { OnAfterObjectUpdate } 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 ReferenceAttributeNode, { referenceAttribute } from './ReferenceAttributeNode.js';
import { buffer } from './BufferNode.js';
import { storage } from './StorageBufferNode.js';
import { instanceIndex } from '../core/IndexNode.js';
import { InterleavedBufferAttribute } from '../../core/InterleavedBufferAttribute.js';
import { DynamicDrawUsage } from '../../constants.js';
const _previousInstanceMatrices = /*@__PURE__*/ new WeakMap();
const _previousMatrices = /*@__PURE__*/ new WeakMap();
// The default limit of vertex buffers in WebGPU.
const MAX_VERTEX_BUFFERS = 8;
/**
* 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.
* @returns {Node} The matrix node.
*/
function createInstanceMatrixNode( builder, instanceMatrix ) {
let instanceMatrixNode;
const matrixCount = Math.max( instanceMatrix.count, 1 );
const isStorageMatrix = instanceMatrix.isStorageInstancedBufferAttribute === true;
if ( isStorageMatrix ) {
instanceMatrixNode = storage( instanceMatrix, 'mat4', matrixCount ).element( instanceIndex );
} else {
const uniformBufferSize = matrixCount * 16 * 4;
if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
} else {
// the columns view the matrices, so both share the same GPU buffer which is freed via instanceMatrix.dispose()
const bufferFn = instanceMatrix.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;
const instanceBuffers = [ 0, 4, 8, 12 ].map( ( offset ) => bufferFn( new InterleavedBufferAttribute( instanceMatrix, 4, offset ) ) );
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.
* @returns {Node} The previous frame instance matrix node.
*/
function getPreviousInstance( instancedMesh, instanceMatrix, builder ) {
let data = _previousInstanceMatrices.get( instancedMesh );
if ( data === undefined ) {
const previousInstanceMatrix = instanceMatrix.clone();
data = {
previousInstanceMatrix,
node: createInstanceMatrixNode( builder, previousInstanceMatrix )
};
_previousInstanceMatrices.set( instancedMesh, data );
instancedMesh.addEventListener( 'dispose', onPreviousInstanceDispose );
}
return data.node;
}
/**
* Frees the GPU buffer of the previous frame instance matrices when the mesh is disposed.
* The matrices are kept since the node refers to them, rendering the mesh again re-creates the buffer.
*
* @param {Object} event - The dispose event.
*/
function onPreviousInstanceDispose( { target: object } ) {
_previousInstanceMatrices.get( object ).previousInstanceMatrix.dispose();
}
/**
* Returns the instance matrices of the previous frame of the given mesh, for motion vectors.
*
* @param {InstancedMesh} object - The instanced mesh.
* @returns {InstancedBufferAttribute} The matrices of the previous frame.
*/
function getPreviousMatrix( object ) {
const matrices = object.instanceMatrix;
let previous = _previousMatrices.get( object );
if ( previous === undefined || previous.count !== matrices.count ) {
if ( previous === undefined ) {
object.addEventListener( 'dispose', onPreviousMatrixDispose );
} else {
previous.dispose();
}
previous = matrices.clone();
_previousMatrices.set( object, previous );
}
return previous;
}
/**
* Disposes the instance matrices of the previous frame when the mesh is disposed.
*
* @param {Object} event - The dispose event.
*/
function onPreviousMatrixDispose( { target: object } ) {
object.removeEventListener( 'dispose', onPreviousMatrixDispose );
_previousMatrices.get( object ).dispose();
_previousMatrices.delete( object );
}
/**
* Binds the instance matrices of the previous frame of the rendered mesh.
*
* @private
* @augments ReferenceAttributeNode
*/
class PreviousInstanceMatrixNode extends ReferenceAttributeNode {
static get type() {
return 'PreviousInstanceMatrixNode';
}
/**
* Constructs a new previous instance matrix node.
*/
constructor() {
super( 'previousInstanceMatrix', 'mat4' );
}
/**
* Returns the instance matrices of the previous frame of the given mesh.
*
* @param {InstancedMesh} object - The instanced mesh.
* @return {InstancedBufferAttribute} The matrices of the previous frame.
*/
getValueFromReference( object ) {
return getPreviousMatrix( object );
}
}
/**
* Returns `true` if the build can bind the instance data of the rendered mesh, so it can
* be shared between instanced meshes.
*
* @param {InstancedMesh} object - The instanced mesh.
* @param {NodeBuilder} builder - The current node builder.
* @returns {boolean} Whether the build can bind the instance data of the rendered mesh.
*/
function canReferenceInstanceData( object, builder ) {
// only render objects bind attributes per object
if ( builder.renderer.isRenderer !== true ) return false;
// the instance data take up to three vertex buffers next to the ones of the geometry
const instanceBuffers = 1 + ( object.instanceColor !== null ? 1 : 0 ) + ( builder.needsPreviousData() ? 1 : 0 );
return Object.keys( object.geometry.attributes ).length + instanceBuffers <= MAX_VERTEX_BUFFERS;
}
/**
* Transforms the position, the previous position and the normal with the given instance matrices.
*
* @param {NodeBuilder} builder - The current node builder.
* @param {Node<mat4>} instanceMatrixNode - The instance matrix.
* @param {?Node<mat4>} previousMatrixNode - The instance matrix of the previous frame, if motion vectors are needed.
*/
function transformInstance( builder, instanceMatrixNode, previousMatrixNode ) {
// POSITION
const instancePosition = instanceMatrixNode.mul( positionLocal ).xyz;
positionLocal.assign( instancePosition );
if ( previousMatrixNode !== null ) {
positionPrevious.assign( previousMatrixNode.mul( positionPrevious ).xyz );
}
// NORMAL
if ( builder.hasGeometryAttribute( 'normal' ) ) {
const instanceNormal = transformNormal( normalLocal, instanceMatrixNode );
normalLocal.assign( instanceNormal );
}
}
/**
* TSL object representing a varying property for the instanced color vector.
*
* @type {VaryingNode<vec3>}
*/
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 {InstancedBufferAttribute|StorageInstancedBufferAttribute} matrices - The instanced transformation matrices.
* @param {?InstancedBufferAttribute|StorageInstancedBufferAttribute} [colors=null] - The optional instanced colors.
*/
export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
const isStorageColor = colors && colors.isStorageInstancedBufferAttribute === true;
const instanceMatrixNode = createInstanceMatrixNode( builder, matrices );
let instanceColorNode = null;
if ( colors ) {
if ( isStorageColor ) {
instanceColorNode = storage( colors, 'vec3', Math.max( colors.count, 1 ) ).element( instanceIndex );
} else {
// bound directly, so the GPU buffer is freed via instanceColor.dispose()
const bufferFn = colors.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;
instanceColorNode = vec3( bufferFn( colors ) );
}
}
let previousInstanceMatrixNode = null;
if ( builder.needsPreviousData() ) {
const instancedMesh = builder.object;
OnAfterObjectUpdate( ( { object } ) => {
const { previousInstanceMatrix } = _previousInstanceMatrices.get( object );
previousInstanceMatrix.array.set( matrices.array );
previousInstanceMatrix.version = matrices.version;
} );
previousInstanceMatrixNode = getPreviousInstance( instancedMesh, matrices, builder );
}
transformInstance( builder, instanceMatrixNode, previousInstanceMatrixNode );
// COLOR
if ( instanceColorNode !== null ) {
instanceColor.assign( instanceColorNode );
}
}, 'void' );
/**
* TSL wrapper for applying instanced mesh rendering setup.
*
* The instance data of the rendered mesh are bound per object via {@link ReferenceAttributeNode},
* so instanced meshes share builds.
*
* @tsl
* @function
* @param {InstancedMesh} instancedMesh - The instanced mesh.
*/
export const instancedMesh = /*@__PURE__*/ Fn( ( [ instancedMesh ], builder ) => {
if ( canReferenceInstanceData( instancedMesh, builder ) ) {
let previousInstanceMatrixNode = null;
if ( builder.needsPreviousData() ) {
OnAfterObjectUpdate( ( { object } ) => {
const previous = getPreviousMatrix( object );
previous.array.set( object.instanceMatrix.array );
previous.version = object.instanceMatrix.version;
} );
previousInstanceMatrixNode = new PreviousInstanceMatrixNode();
}
transformInstance( builder, referenceAttribute( 'instanceMatrix', 'mat4' ), previousInstanceMatrixNode );
if ( instancedMesh.instanceColor !== null ) {
instanceColor.assign( referenceAttribute( 'instanceColor', 'vec3' ) );
}
} else {
builder.objectSpecific = true;
instance( instancedMesh.instanceMatrix, instancedMesh.instanceColor );
}
}, 'void' );