three
Version:
JavaScript 3D library
955 lines (635 loc) • 37.2 kB
JavaScript
import {
StorageBufferAttribute,
IndirectStorageBufferAttribute,
Mesh,
BoxGeometry,
BufferGeometry,
Float32BufferAttribute
} from 'three/webgpu';
import {
Fn, If, Loop, vec2, vec3, vec4, mat3, uint, float, int, min, max, floor, sqrt, select, struct, transformNormal,
storage, uniform, instanceIndex, vertexIndex, screenCoordinate, varyingProperty,
positionGeometry, cameraViewMatrix, cameraProjectionMatrix, normalize, context, overrideNodes,
positionLocal, positionWorld, positionWorldDirection, positionView, positionViewDirection,
normalLocal, normalViewGeometry, normalWorldGeometry, tangentViewFrame, bitangentViewFrame, modelWorldMatrix, modelViewMatrix, modelPosition, atomicMax, atomicAdd, atomicStore, atomicLoad
} from 'three/tsl';
import { MeshletDrawingPipeline, MeshletBatch, cacheAttributes, createDebugMaterial, createDrawMaterial, createOcclusion } from './MeshletDrawingPipeline.js';
import { MESHLET_SIZE } from '../utils/MeshletGeometryUtils.js';
// visibility buffer packing — the depth occupies the bits above each payload
const TRIANGLE_INDEX_BITS = 16;
const INSTANCE_INDEX_BITS = 17;
const TRIANGLE_INDEX_MASK = 2 ** TRIANGLE_INDEX_BITS - 1;
const INSTANCE_INDEX_MASK = 2 ** INSTANCE_INDEX_BITS - 1;
const DEPTH_TRI_MAX = 2 ** ( 32 - TRIANGLE_INDEX_BITS ) - 1;
const DEPTH_INST_MAX = 2 ** ( 32 - INSTANCE_INDEX_BITS ) - 1;
const MAX_TRIANGLES = TRIANGLE_INDEX_MASK + 1;
const MAX_INSTANCES = INSTANCE_INDEX_MASK + 1;
// the large triangles of one batch drawn by the hardware rasterizer per frame, limited by the vertices of the pulled geometry
const MAX_HW_TRIANGLES = 65536;
let _resolveGeometry = null;
let _fullscreenGeometry = null;
// the corners of a triangle deformed by the position node of the material, see deformTriangle()
const DeformedTriangle = /*@__PURE__*/ struct( { positions: 'mat3', normals: 'mat3' }, 'DeformedTriangle' );
const edgeFunction = /*@__PURE__*/ Fn( ( [ a, b, c ] ) => {
return c.y.sub( a.y ).mul( b.x.sub( a.x ) ).sub( c.x.sub( a.x ).mul( b.y.sub( a.y ) ) );
} );
/**
* Returns the positions and the normals of the corners of a triangle deformed by the position node of the material, in
* the space of the object: the node is evaluated per corner with the position, the normal and the attributes of the
* vertex, and the position and the normal transformed by the instance matrix, like the regular instancing of node
* materials. The normal assigned to `normalLocal` by the position node is the normal of the corner.
*
* @private
* @param {Node<vec3>} positionNode - The position node of the material.
* @param {Object} vertex - The functions reading the position, the normal and the attributes of a vertex.
* @param {Array<Node<uint>>} corners - The addresses of the vertices of the corners.
* @param {Node<mat4>} instanceMatrix - The instance matrix, identity for objects which are not instanced meshes.
* @param {Node<uint>} instance - The instance index of the material.
* @return {Node} The deformed positions and normals, one column per corner, see DeformedTriangle.
*/
function deformTriangle( positionNode, { position, normal, vertexUv, hasAttribute, attribute }, corners, instanceMatrix, instance ) {
return Fn( () => {
const address = uint( 0 ).toVar();
const cornerNormal = vec3( 0 ).toVar();
const positions = [ vec3( 0 ).toVar(), vec3( 0 ).toVar(), vec3( 0 ).toVar() ];
const normals = [ vec3( 0 ).toVar(), vec3( 0 ).toVar(), vec3( 0 ).toVar() ];
const geometryPosition = position( address );
const geometryNormal = normal( address );
const getAttribute = cacheAttributes( ( name ) => {
if ( name === 'position' ) return geometryPosition;
if ( name === 'normal' ) return geometryNormal;
if ( name === 'uv' ) return vertexUv( address );
return hasAttribute( name ) ? attribute( name, address ) : null;
} );
const deformed = context( positionNode.overrideNodes( [
[ positionLocal, instanceMatrix.mul( vec4( geometryPosition, 1.0 ) ).xyz ],
[ normalLocal, cornerNormal ],
[ instanceIndex, instance ]
] ), { getAttribute } );
// the node is built once, in the loop over the corners
Loop( { start: 0, end: 3, type: 'int' }, ( { i } ) => {
address.assign( select( i.equal( 0 ), corners[ 0 ], select( i.equal( 1 ), corners[ 1 ], corners[ 2 ] ) ) );
cornerNormal.assign( transformNormal( geometryNormal, instanceMatrix ) );
const value = vec3( deformed ).toVar();
If( i.equal( 0 ), () => {
positions[ 0 ].assign( value );
normals[ 0 ].assign( cornerNormal );
} ).ElseIf( i.equal( 1 ), () => {
positions[ 1 ].assign( value );
normals[ 1 ].assign( cornerNormal );
} ).Else( () => {
positions[ 2 ].assign( value );
normals[ 2 ].assign( cornerNormal );
} );
} );
return DeformedTriangle( mat3( positions[ 0 ], positions[ 1 ], positions[ 2 ] ), mat3( normals[ 0 ], normals[ 1 ], normals[ 2 ] ) );
} )();
}
/**
* Returns a context which feeds the surface of the merged object covering the current fragment into
* the material graph: position and normal in local, world and view space, the model matrices and the
* first uv channel. The nodes of the material evaluate the merged object instead of the drawing mesh.
* If a uv gradient is given, textures sampled with the first uv channel use it instead of the hardware
* derivatives, which are invalid if neighbor fragments belong to other triangles. The same applies to
* the tangent frame of normal maps without precomputed tangents. Other attributes of the geometry are
* provided by the given attribute function. The surface only exists in the fragment stage, so the nodes
* are overridden in the fragment stage and the varyings derived from them are evaluated there as well.
*
* @private
* @param {Object} surface - The reconstructed surface.
* @return {ContextNode} The context node.
*/
function createSurfaceContext( { localPosition, localNormal, worldPosition, worldNormal, uv, uvGradient = null, tangentFrame = null, matrixWorld, attribute } ) {
const viewPosition = cameraViewMatrix.mul( vec4( worldPosition, 1.0 ) ).xyz;
const overrides = [
[ positionLocal, localPosition ],
[ positionWorld, worldPosition ],
[ positionWorldDirection, normalize( matrixWorld.mul( vec4( localPosition, 0.0 ) ).xyz ) ],
[ positionView, viewPosition ],
[ positionViewDirection, viewPosition.negate().normalize() ],
[ normalLocal, normalize( localNormal ) ],
[ normalWorldGeometry, normalize( worldNormal ) ],
[ normalViewGeometry, cameraViewMatrix.mul( vec4( worldNormal, 0.0 ) ).xyz.normalize() ],
[ modelWorldMatrix, matrixWorld ],
[ modelViewMatrix, cameraViewMatrix.mul( matrixWorld ) ],
[ modelPosition, matrixWorld.mul( vec4( 0.0, 0.0, 0.0, 1.0 ) ).xyz ]
];
if ( tangentFrame !== null ) overrides.push( [ tangentViewFrame, tangentFrame[ 0 ] ], [ bitangentViewFrame, tangentFrame[ 1 ] ] );
return overrideNodes( overrides, context( {
getAttribute: cacheAttributes( ( name ) => name === 'uv' ? uv : attribute( name ) ),
getTextureGradient: uvGradient !== null ? () => uvGradient : null
} ), 'fragment' );
}
/**
* Creates the visibility buffers of a view, shared by all batches of a render list, and the compute
* node clearing them. The instances of all batches share one index range, each batch owns a slice.
*
* @private
* @param {number} width - The width of the view in pixels.
* @param {number} height - The height of the view in pixels.
* @return {Object} The view.
*/
function createView( width, height ) {
const pixels = width * height;
// depth(16) | triangle(16) and depth(15) | instance(17)
const screenTriAttribute = new StorageBufferAttribute( new Uint32Array( pixels ), 1 );
const screenInstAttribute = new StorageBufferAttribute( new Uint32Array( pixels ), 1 );
const screenTriAtomic = storage( screenTriAttribute, 'uint', pixels ).toAtomic();
const screenInstAtomic = storage( screenInstAttribute, 'uint', pixels ).toAtomic();
const computeClear = Fn( () => {
atomicStore( screenTriAtomic.element( instanceIndex ), uint( 0 ) );
atomicStore( screenInstAtomic.element( instanceIndex ), uint( 0 ) );
} )().compute( pixels, [ 256 ] ).setName( 'VisibilityBufferDrawingPipeline Clear' );
return {
width,
height,
screenTriAtomic,
screenTriRead: storage( screenTriAttribute, 'uint', pixels ).toReadOnly(),
screenInstAtomic,
screenInstRead: storage( screenInstAttribute, 'uint', pixels ).toReadOnly(),
computeClear,
dispose() {
computeClear.dispose();
screenTriAttribute.dispose();
screenInstAttribute.dispose();
}
};
}
/**
* A batch drawing the visible clusters into a visibility buffer: a compute shader rasterizes the small
* triangles in software and queues the large ones for the hardware rasterizer. A resolve draws the bounding
* box of each object and shades the fragments whose visible triangle belongs to the object.
*
* @private
* @augments MeshletBatch
*/
class VisibilityBufferBatch extends MeshletBatch {
/**
* Creates the software rasterizer, the hardware path of the large triangles and the resolve.
*
* @param {Object} shared - The nodes and resources of the batch.
*/
setupDraw( shared ) {
const {
createAttribute, clusters, material, renderer, receiveShadow, debug,
position, normal, vertexUv, hasAttribute, attribute, indexBuffer, instanceWorld, instanceMvp, objectWorld, instanceLocal, instanceData,
workQueue, workQueueCountRead, maxWorkItems, projScreenMatrix, viewSize
} = shared;
// the position node of the material is evaluated per corner of the triangles by the software rasterizer and by the
// resolve. the world matrices of the culling approximate the deformation, the real ones are the matrices of the
// objects. the instance index of the material is the index of the instance in its instanced mesh
const deformed = this.deformed;
const vertex = { position, normal, vertexUv, hasAttribute, attribute };
const stride = this.instanceDataStride;
const deformCorners = ( corners, instId ) => deformTriangle( material.positionNode, vertex, corners, instanceLocal.element( instId ), uint( instanceData.element( instId.mul( stride ) ).x ) );
// the batches of the visibility buffer draw one geometry, the triangles of the visibility buffer are indexed
// from the first triangle of the geometry in the geometry pool
const triangleBase = this.triangleBase = uniform( this.pool.add( clusters ), 'uint' );
this.maxRasterSize = uniform( 8, 'int' );
// the slice of the instance index range of the view owned by this batch
this.instanceOffset = uniform( 0, 'uint' );
const { maxRasterSize, instanceOffset } = this;
// visibility buffers shared by all batches of the view
const { screenTriAtomic, screenTriRead, screenInstAtomic, screenInstRead } = this.view;
const dispatchAttribute = createAttribute( new Uint32Array( 3 ), 3, IndirectStorageBufferAttribute );
const dispatchBuffer = storage( dispatchAttribute, 'uint', 3 );
// the software rasterizer sends large triangles to the hardware queue, the counter followed by [ instance, triangle ] pairs
const hwQueueAttribute = createAttribute( new Uint32Array( 1 + MAX_HW_TRIANGLES * 2 ), 1 );
const hwQueueAtomic = storage( hwQueueAttribute, 'uint', 1 + MAX_HW_TRIANGLES * 2 ).toAtomic();
const hwQueueRead = storage( hwQueueAttribute, 'uint', 1 + MAX_HW_TRIANGLES * 2 ).toReadOnly();
const hwDrawAttribute = createAttribute( new Uint32Array( 4 ), 4, IndirectStorageBufferAttribute );
const hwDrawBuffer = storage( hwDrawAttribute, 'uint', 4 );
const computeHWReset = Fn( () => {
atomicStore( hwQueueAtomic.element( 0 ), uint( 0 ) );
} )().compute( 1 ).setName( 'VisibilityBufferDrawingPipeline Reset' );
// indirect dispatch arguments, split into 2D if exceeding 65535 workgroups
const computeDispatch = Fn( () => {
const workgroups = min( workQueueCountRead.element( 0 ), maxWorkItems );
const maxDimension = uint( 65535 );
dispatchBuffer.element( 0 ).assign( min( workgroups, maxDimension ) );
dispatchBuffer.element( 1 ).assign( workgroups.add( maxDimension ).sub( 1 ).div( maxDimension ) );
dispatchBuffer.element( 2 ).assign( 1 );
} )().compute( 1 ).setName( 'VisibilityBufferDrawingPipeline Dispatch' );
// software rasterizer — one thread per triangle of a cluster
const computeRasterize = Fn( () => {
const totalThreads = min( workQueueCountRead.element( 0 ), maxWorkItems ).mul( MESHLET_SIZE );
If( instanceIndex.lessThan( totalThreads ), () => {
const workItem = workQueue.element( instanceIndex.div( MESHLET_SIZE ) );
const instId = workItem.x;
const lodTriangleStart = workItem.y;
const lodNumTriangles = workItem.z;
const chunk = workItem.w;
const lodTriangle = chunk.mul( MESHLET_SIZE ).add( instanceIndex.mod( MESHLET_SIZE ) );
If( lodTriangle.lessThan( lodNumTriangles ), () => {
const megaTriangleIndex = lodTriangleStart.add( lodTriangle );
const indexOffset = megaTriangleIndex.mul( 3 );
const corners = [ indexBuffer.element( indexOffset ), indexBuffer.element( indexOffset.add( 1 ) ), indexBuffer.element( indexOffset.add( 2 ) ) ];
let p0, p1, p2;
if ( deformed === true ) {
const deformedPositions = deformCorners( corners, instId ).get( 'positions' );
const mvp = projScreenMatrix.mul( objectWorld.element( instId ) ).toVar();
p0 = mvp.mul( vec4( deformedPositions.element( 0 ), 1.0 ) );
p1 = mvp.mul( vec4( deformedPositions.element( 1 ), 1.0 ) );
p2 = mvp.mul( vec4( deformedPositions.element( 2 ), 1.0 ) );
} else {
const mvp = instanceMvp.element( instId );
p0 = mvp.mul( vec4( position( corners[ 0 ] ), 1.0 ) );
p1 = mvp.mul( vec4( position( corners[ 1 ] ), 1.0 ) );
p2 = mvp.mul( vec4( position( corners[ 2 ] ), 1.0 ) );
}
const enqueueHardware = () => {
const hwCount = atomicAdd( hwQueueAtomic.element( 0 ), 1 );
If( hwCount.lessThan( MAX_HW_TRIANGLES ), () => {
const slot = hwCount.mul( 2 ).add( 1 );
atomicStore( hwQueueAtomic.element( slot ), instId );
atomicStore( hwQueueAtomic.element( slot.add( 1 ) ), megaTriangleIndex );
} );
};
// triangles crossing the near plane need clipping, so they go to the hardware rasterizer
If( p0.w.greaterThan( 0.0 ).and( p1.w.greaterThan( 0.0 ) ).and( p2.w.greaterThan( 0.0 ) ), () => {
const ndc0 = p0.xyz.div( p0.w );
const ndc1 = p1.xyz.div( p1.w );
const ndc2 = p2.xyz.div( p2.w );
// back-face culling
If( edgeFunction( ndc0, ndc1, ndc2 ).greaterThan( 0.0 ), () => {
const ndcMinX = min( ndc0.x, min( ndc1.x, ndc2.x ) );
const ndcMaxX = max( ndc0.x, max( ndc1.x, ndc2.x ) );
const ndcMinY = min( ndc0.y, min( ndc1.y, ndc2.y ) );
const ndcMaxY = max( ndc0.y, max( ndc1.y, ndc2.y ) );
If( ndcMaxX.greaterThan( - 1.0 ).and( ndcMinX.lessThan( 1.0 ) ).and( ndcMaxY.greaterThan( - 1.0 ) ).and( ndcMinY.lessThan( 1.0 ) ), () => {
const w = viewSize.x;
const h = viewSize.y;
const s0 = ndc0.xy.add( 1.0 ).mul( 0.5 ).mul( vec2( w, h ) );
const s1 = ndc1.xy.add( 1.0 ).mul( 0.5 ).mul( vec2( w, h ) );
const s2 = ndc2.xy.add( 1.0 ).mul( 0.5 ).mul( vec2( w, h ) );
const startX = int( floor( max( 0.0, min( s0.x, min( s1.x, s2.x ) ) ) ) );
const endX = int( floor( min( w.sub( 1.0 ), max( s0.x, max( s1.x, s2.x ) ) ) ) );
const startY = int( floor( max( 0.0, min( s0.y, min( s1.y, s2.y ) ) ) ) );
const endY = int( floor( min( h.sub( 1.0 ), max( s0.y, max( s1.y, s2.y ) ) ) ) );
// only small triangles are rasterized in software, the others go to the hardware queue
If( startX.lessThanEqual( endX ).and( startY.lessThanEqual( endY ) ).and( endX.sub( startX ).lessThanEqual( maxRasterSize ) ).and( endY.sub( startY ).lessThanEqual( maxRasterSize ) ), () => {
const area = edgeFunction( s0, s1, s2 );
const stepX_w0 = s1.y.sub( s2.y );
const stepY_w0 = s2.x.sub( s1.x );
const stepX_w1 = s2.y.sub( s0.y );
const stepY_w1 = s0.x.sub( s2.x );
const stepX_w2 = s0.y.sub( s1.y );
const stepY_w2 = s1.x.sub( s0.x );
// top-left rule for watertight edges
const bias0 = stepX_w0.lessThan( 0.0 ).or( stepX_w0.equal( 0.0 ).and( stepY_w0.greaterThan( 0.0 ) ) ).select( 0.0, - 1e-5 );
const bias1 = stepX_w1.lessThan( 0.0 ).or( stepX_w1.equal( 0.0 ).and( stepY_w1.greaterThan( 0.0 ) ) ).select( 0.0, - 1e-5 );
const bias2 = stepX_w2.lessThan( 0.0 ).or( stepX_w2.equal( 0.0 ).and( stepY_w2.greaterThan( 0.0 ) ) ).select( 0.0, - 1e-5 );
const pStart = vec2( float( startX ).add( 0.5 ), float( startY ).add( 0.5 ) );
const row_w0 = edgeFunction( s1, s2, pStart ).add( bias0 ).toVar();
const row_w1 = edgeFunction( s2, s0, pStart ).add( bias1 ).toVar();
const row_w2 = edgeFunction( s0, s1, pStart ).add( bias2 ).toVar();
// incremental depth
const row_z = row_w0.div( area ).mul( ndc0.z ).add( row_w1.div( area ).mul( ndc1.z ) ).add( row_w2.div( area ).mul( ndc2.z ) ).toVar();
const stepX_z = stepX_w0.div( area ).mul( ndc0.z ).add( stepX_w1.div( area ).mul( ndc1.z ) ).add( stepX_w2.div( area ).mul( ndc2.z ) );
const stepY_z = stepY_w0.div( area ).mul( ndc0.z ).add( stepY_w1.div( area ).mul( ndc1.z ) ).add( stepY_w2.div( area ).mul( ndc2.z ) );
Loop( { name: 'y', type: 'int', start: startY, end: endY, condition: '<=' }, ( { y } ) => {
const w0 = row_w0.toVar();
const w1 = row_w1.toVar();
const w2 = row_w2.toVar();
const z = row_z.toVar();
Loop( { name: 'x', type: 'int', start: startX, end: endX, condition: '<=' }, ( { x } ) => {
If( w0.greaterThanEqual( 0.0 ).and( w1.greaterThanEqual( 0.0 ) ).and( w2.greaterThanEqual( 0.0 ) ).and( z.greaterThanEqual( 0.0 ) ).and( z.lessThanEqual( 1.0 ) ), () => {
// depth with a fourth-root distribution, packed above each payload
const zEncoded = sqrt( sqrt( float( 1.0 ).sub( z ) ) );
const depthTri = uint( zEncoded.mul( DEPTH_TRI_MAX ) );
const depthInst = uint( zEncoded.mul( DEPTH_INST_MAX ) );
const pixelIndex = uint( y ).mul( uint( w ) ).add( uint( x ) );
// the depth occupies the high bits, so atomicMax resolves the depth test and the payload together
If( depthTri.greaterThanEqual( atomicLoad( screenTriAtomic.element( pixelIndex ) ).shiftRight( TRIANGLE_INDEX_BITS ) ), () => {
atomicMax( screenTriAtomic.element( pixelIndex ), depthTri.shiftLeft( TRIANGLE_INDEX_BITS ).bitOr( megaTriangleIndex.sub( triangleBase ).bitAnd( TRIANGLE_INDEX_MASK ) ) );
atomicMax( screenInstAtomic.element( pixelIndex ), depthInst.shiftLeft( INSTANCE_INDEX_BITS ).bitOr( instanceOffset.add( instId ) ) );
} );
} );
w0.addAssign( stepX_w0 );
w1.addAssign( stepX_w1 );
w2.addAssign( stepX_w2 );
z.addAssign( stepX_z );
} );
row_w0.addAssign( stepY_w0 );
row_w1.addAssign( stepY_w1 );
row_w2.addAssign( stepY_w2 );
row_z.addAssign( stepY_z );
} );
} ).ElseIf( startX.lessThanEqual( endX ).and( startY.lessThanEqual( endY ) ), enqueueHardware );
} );
} );
} ).ElseIf( p0.w.greaterThan( 0.0 ).or( p1.w.greaterThan( 0.0 ) ).or( p2.w.greaterThan( 0.0 ) ), enqueueHardware );
} );
} );
} )().compute( dispatchAttribute ).setName( 'VisibilityBufferDrawingPipeline Rasterize' );
// indirect draw arguments of the hardware path
const computeHWArgs = Fn( () => {
hwDrawBuffer.element( 0 ).assign( min( atomicLoad( hwQueueAtomic.element( 0 ) ), uint( MAX_HW_TRIANGLES ) ).mul( 3 ) );
hwDrawBuffer.element( 1 ).assign( uint( 1 ) );
hwDrawBuffer.element( 2 ).assign( uint( 0 ) );
hwDrawBuffer.element( 3 ).assign( uint( 0 ) );
} )().compute( 1 ).setName( 'VisibilityBufferDrawingPipeline HW Args' );
this.computeNodes.push( computeHWReset, computeDispatch, computeRasterize, computeHWArgs );
// hardware path — the large triangles queued by the software rasterizer
const slot = vertexIndex.div( 3 ).mul( 2 ).add( 1 );
this.hwMesh = this.createPulledMesh( hwDrawAttribute, {
instance: hwQueueRead.element( slot ),
triangle: hwQueueRead.element( slot.add( 1 ) ),
corner: vertexIndex.mod( 3 )
}, shared );
// resolve — shades the visibility buffer through the material graph
// the resolve draws the bounding box of each object, so every fragment is only shaded by the box of
// the object visible in it — a fullscreen resolve would run the material for the whole screen per batch.
// the back faces cover the object even if the camera is inside its box. the winding of the box is
// reversed instead of using BackSide, which would negate the normals of the material.
if ( _resolveGeometry === null ) {
_resolveGeometry = new BoxGeometry();
const index = _resolveGeometry.index;
for ( let i = 0; i < index.count; i += 3 ) {
const b = index.getX( i + 1 );
index.setX( i + 1, index.getX( i + 2 ) );
index.setX( i + 2, b );
}
}
const vResolveInstance = varyingProperty( 'uint', 'vVisibilityBufferInstance' );
const boxMin = uniform( clusters.boundingBox.min.clone() );
const boxSize = uniform( clusters.boundingBox.max.clone().sub( clusters.boundingBox.min ) );
const resolveVertex = Fn( () => {
vResolveInstance.assign( instanceIndex );
if ( deformed === true ) {
// the box of the deformed positions: the world matrix of the culling approximates the deformation, the box
// grows by the margin of the deformation. deformations which can't be bounded are resolved fullscreen
const bounds = this.culling.instanceBounds.element( this.rangeNodes.dataOffset.add( instanceIndex ) );
const margin = bounds.x.div( bounds.w.abs() );
const boxPosition = positionGeometry.add( 0.5 ).mul( boxSize.add( margin.mul( 2 ) ) ).add( boxMin.sub( margin ) );
const clipPosition = cameraProjectionMatrix.mul( cameraViewMatrix ).mul( instanceWorld.element( instanceIndex ) ).mul( vec4( boxPosition, 1.0 ) );
return select( this.unbounded.greaterThan( 0 ), vec4( 0 ), clipPosition );
}
const boxPosition = positionGeometry.add( 0.5 ).mul( boxSize ).add( boxMin );
return cameraProjectionMatrix.mul( cameraViewMatrix ).mul( instanceWorld.element( instanceIndex ) ).mul( vec4( boxPosition, 1.0 ) );
} )();
// the rasterizer addresses the screen bottom-up, screenCoordinate is top-down
const flippedY = viewSize.y.sub( screenCoordinate.y );
const pixelIndex = uint( flippedY ).mul( uint( viewSize.x ) ).add( uint( screenCoordinate.x ) );
const packedTri = screenTriRead.element( pixelIndex );
const megaTriangleIndex = packedTri.bitAnd( TRIANGLE_INDEX_MASK ).add( triangleBase );
// the fragment is shaded if the object visible in it is the object of the box
const globalInstance = screenInstRead.element( pixelIndex ).bitAnd( INSTANCE_INDEX_MASK );
const ownedByBox = globalInstance.equal( instanceOffset.add( vResolveInstance ) );
const instId = globalInstance.sub( instanceOffset );
// the fullscreen resolve shades the fragments of all objects of the batch
this.resolveCount = uniform( 0, 'uint' );
const ownedByBatch = instId.lessThan( this.resolveCount );
const i0 = indexBuffer.element( megaTriangleIndex.mul( 3 ) );
const i1 = indexBuffer.element( megaTriangleIndex.mul( 3 ).add( 1 ) );
const i2 = indexBuffer.element( megaTriangleIndex.mul( 3 ).add( 2 ) );
let matrixWorld, l0, l1, l2, n0, n1, n2;
if ( deformed === true ) {
const deformedTriangle = deformCorners( [ i0, i1, i2 ], instId );
const deformedPositions = deformedTriangle.get( 'positions' );
const deformedNormals = deformedTriangle.get( 'normals' );
matrixWorld = objectWorld.element( instId );
l0 = deformedPositions.element( 0 );
l1 = deformedPositions.element( 1 );
l2 = deformedPositions.element( 2 );
n0 = deformedNormals.element( 0 );
n1 = deformedNormals.element( 1 );
n2 = deformedNormals.element( 2 );
} else {
matrixWorld = instanceWorld.element( instId );
l0 = position( i0 );
l1 = position( i1 );
l2 = position( i2 );
n0 = normal( i0 );
n1 = normal( i1 );
n2 = normal( i2 );
}
const p0 = projScreenMatrix.mul( matrixWorld ).mul( vec4( l0, 1.0 ) );
const p1 = projScreenMatrix.mul( matrixWorld ).mul( vec4( l1, 1.0 ) );
const p2 = projScreenMatrix.mul( matrixWorld ).mul( vec4( l2, 1.0 ) );
const screenScale = vec2( viewSize.x, viewSize.y );
const s0 = p0.xy.div( p0.w ).add( 1.0 ).mul( 0.5 ).mul( screenScale );
const s1 = p1.xy.div( p1.w ).add( 1.0 ).mul( 0.5 ).mul( screenScale );
const s2 = p2.xy.div( p2.w ).add( 1.0 ).mul( 0.5 ).mul( screenScale );
const p = vec2( screenCoordinate.x, flippedY );
// perspective correct barycentrics from the edge functions weighted by the inverse clip w
const e0 = edgeFunction( s1, s2, p );
const e1 = edgeFunction( s2, s0, p );
const e2 = edgeFunction( s0, s1, p );
const q0 = float( 1.0 ).div( p0.w );
const q1 = float( 1.0 ).div( p1.w );
const q2 = float( 1.0 ).div( p2.w );
const sum = e0.mul( q0 ).add( e1.mul( q1 ) ).add( e2.mul( q2 ) );
const safeSum = sum.equal( 0.0 ).select( 1.0, sum );
const interpolate = ( a0, a1, a2 ) => a0.mul( e0.mul( q0 ) ).add( a1.mul( e1.mul( q1 ) ) ).add( a2.mul( e2.mul( q2 ) ) ).div( safeSum );
// analytic screen-space derivatives, neighbor fragments can belong to other triangles —
// the derivatives of the edge functions along the screen x and y axes, with the same
// orientation as the hardware derivatives
const dx = ( a0, a1, a2, value ) => derivative( a0, a1, a2, value, s2.y.sub( s1.y ), s0.y.sub( s2.y ), s1.y.sub( s0.y ) );
const dy = ( a0, a1, a2, value ) => derivative( a0, a1, a2, value, s1.x.sub( s2.x ), s2.x.sub( s0.x ), s0.x.sub( s1.x ) );
const derivative = ( a0, a1, a2, value, d0, d1, d2 ) => d0.mul( q0 ).mul( a0.sub( value ) ).add( d1.mul( q1 ).mul( a1.sub( value ) ) ).add( d2.mul( q2 ).mul( a2.sub( value ) ) ).div( safeSum );
const uv0 = vertexUv( i0 );
const uv1 = vertexUv( i1 );
const uv2 = vertexUv( i2 );
const uv = interpolate( uv0, uv1, uv2 );
const uvDx = dx( uv0, uv1, uv2, uv );
const uvDy = dy( uv0, uv1, uv2, uv );
const localPosition = interpolate( l0, l1, l2 );
const localNormal = interpolate( n0, n1, n2 );
const worldPosition = matrixWorld.mul( vec4( localPosition, 1.0 ) ).xyz;
const worldNormal = matrixWorld.mul( vec4( localNormal, 0.0 ) ).xyz;
// tangent frame of normal maps from the analytic derivatives, see TangentUtils
const modelView = cameraViewMatrix.mul( matrixWorld );
const v0 = modelView.mul( vec4( l0, 1.0 ) ).xyz;
const v1 = modelView.mul( vec4( l1, 1.0 ) ).xyz;
const v2 = modelView.mul( vec4( l2, 1.0 ) ).xyz;
const viewPosition = interpolate( v0, v1, v2 );
const viewNormal = cameraViewMatrix.mul( vec4( worldNormal, 0.0 ) ).xyz.normalize();
const q1perp = dy( v0, v1, v2, viewPosition ).cross( viewNormal );
const q0perp = viewNormal.cross( dx( v0, v1, v2, viewPosition ) );
const tangent = q1perp.mul( uvDx.x ).add( q0perp.mul( uvDy.x ) );
const bitangent = q1perp.mul( uvDx.y ).add( q0perp.mul( uvDy.y ) );
const det = tangent.dot( tangent ).max( bitangent.dot( bitangent ) );
const frameScale = det.equal( 0.0 ).select( 0.0, det.inverseSqrt() );
// depth from the high bits with the fourth-root distribution reversed, zero means not covered
const depthTri = packedTri.shiftRight( TRIANGLE_INDEX_BITS );
const depthEncoded = float( depthTri ).div( DEPTH_TRI_MAX );
const depthSquared = depthEncoded.mul( depthEncoded );
const createResolveMaterial = ( vertexNode, owned ) => {
let resolveMaterial;
if ( debug === true ) {
resolveMaterial = createDebugMaterial( megaTriangleIndex.div( MESHLET_SIZE ), instId );
} else {
resolveMaterial = createDrawMaterial( material, renderer );
resolveMaterial.positionNode = null;
resolveMaterial.receivedShadowPositionNode = worldPosition;
resolveMaterial.contextNode = createSurfaceContext( {
localPosition,
localNormal,
worldPosition,
worldNormal,
uv,
uvGradient: [ uvDx, uvDy ],
tangentFrame: [ tangent.mul( frameScale ), bitangent.mul( frameScale ) ],
matrixWorld,
attribute: ( name ) => {
return hasAttribute( name ) ? interpolate( attribute( name, i0 ), attribute( name, i1 ), attribute( name, i2 ) ) : null;
}
} );
}
resolveMaterial.vertexNode = vertexNode;
resolveMaterial.depthNode = float( 1.0 ).sub( depthSquared.mul( depthSquared ) );
resolveMaterial.maskNode = depthTri.notEqual( uint( 0 ) ).and( owned );
return resolveMaterial;
};
this.resolveMesh = new Mesh( _resolveGeometry, createResolveMaterial( resolveVertex, ownedByBox ) );
this.resolveMesh.frustumCulled = false;
this.resolveMesh.receiveShadow = receiveShadow;
this.meshes.push( this.resolveMesh, this.hwMesh );
// deformations which can't be bounded are resolved by one fullscreen triangle instead of the boxes
if ( deformed === true ) {
if ( _fullscreenGeometry === null ) {
_fullscreenGeometry = new BufferGeometry();
_fullscreenGeometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, - 1, 0, 3, - 1, 0, - 1, 3, 0 ], 3 ) );
}
const fullscreenVertex = select( this.unbounded.greaterThan( 0 ), vec4( positionGeometry.xy, 0.0, 1.0 ), vec4( 0 ) );
this.fullscreenMesh = new Mesh( _fullscreenGeometry, createResolveMaterial( fullscreenVertex, ownedByBatch ) );
this.fullscreenMesh.frustumCulled = false;
this.fullscreenMesh.receiveShadow = receiveShadow;
this.meshes.push( this.fullscreenMesh );
}
}
update( ...parameters ) {
super.update( ...parameters );
// Compaction can move this geometry without rebuilding its batch.
this.triangleBase.value = this.pool.add( this.clusters );
const pipeline = parameters[ 1 ];
this.maxRasterSize.value = pipeline.maxRasterSize;
this.resolveCount.value = this.count;
}
}
/**
* A meshlet drawing pipeline with a hybrid rasterizer. Like {@link MeshletDrawingPipeline}, compute shaders select the
* level of detail and cull objects and clusters, but the visible clusters are rasterized into a visibility
* buffer: small triangles in software by a compute shader, large triangles by the hardware rasterizer. A
* resolve pass shades the visibility buffer with the material of the meshes: its node graph is evaluated on
* the reconstructed surface (position, normal, model matrices and the attributes of the geometry) with
* analytic derivatives, so custom nodes, lights, shadows and the environment work as usual and the meshes
* depth test against other objects.
*
* The visibility buffer stores the triangle in 16 bits, so only the levels of detail whose triangles fit
* into this range are used. Geometries whose most detailed level doesn't fit are drawn by the hardware
* pipeline of {@link MeshletDrawingPipeline}. The resolve only writes the color, so render calls with MRT, like the
* velocity of TRAA, draw all clusters with the hardware pipeline. The position node of the material is evaluated
* per corner of the triangles by the software rasterizer and by the resolve.
*
* ```js
* renderer.drawingPipeline = new VisibilityBufferDrawingPipeline();
* ```
*
* This class is experimental and its interface might change.
*
* @augments MeshletDrawingPipeline
*/
class VisibilityBufferDrawingPipeline extends MeshletDrawingPipeline {
/**
* Constructs a new visibility buffer drawing pipeline.
*
* @param {Object} [parameters] - The configuration parameter.
* @param {number} [parameters.lodThreshold=1] - The projected error in pixels below which a simpler level of detail is selected.
* @param {number} [parameters.shadowLodThreshold=1] - The projected error in shadow map texels below which a simpler level of detail is selected for shadow casters.
* @param {number} [parameters.maxRasterSize=8] - The maximum screen-space size in pixels of triangles rasterized in software.
* @param {boolean} [parameters.debug=false] - Whether the clusters should be shown with a flat color each instead of the material.
* @param {boolean} [parameters.occlusionCulling=true] - Whether the clusters hidden by other clusters should be culled.
*/
constructor( { lodThreshold, shadowLodThreshold, maxRasterSize = 8, debug, occlusionCulling } = {} ) {
super( { lodThreshold, shadowLodThreshold, debug, occlusionCulling } );
this._hardware = false;
/**
* The maximum screen-space size in pixels of triangles rasterized in software.
* Larger triangles are drawn by the hardware rasterizer. The software rasterizer
* loops over the pixels of a triangle in one thread, so it only pays off for small
* triangles.
*
* @type {number}
* @default 8
*/
this.maxRasterSize = maxRasterSize;
}
/**
* The maximum number of objects per render list, limited by the instance index of the visibility buffer.
*
* @type {number}
* @readonly
*/
get maxInstances() {
return MAX_INSTANCES;
}
updateView( record, width, height ) {
// shadow passes are drawn by the hardware rasterizer of the base pipeline
if ( this._shadowPass === true ) return null;
// the resolve only writes the color and the software rasterizer doesn't apply the deformation of the render call:
// render calls with MRT or deformed positions draw all clusters with the hardware rasterizer
const hardware = this.renderer.getMRT() !== null || this._boundsNode !== null;
if ( record.view !== null && record.view.hardware !== hardware ) {
record.view.dispose();
record.view = null;
}
this._hardware = hardware;
return super.updateView( record, width, height );
}
_isSupportedMaterial( material ) {
// the software rasterizer doesn't discard fragments
// the resolve doesn't write the MRT outputs of the material
if ( material.isNodeMaterial === true && ( ( material.maskNode !== null && material.maskNode !== undefined ) || ( material.mrtNode !== null && material.mrtNode !== undefined ) ) ) return false;
return super._isSupportedMaterial( material );
}
_canMergeLevels() {
// the resolve shades a batch with one material, the levels of LOD objects are drawn as separate objects
return false;
}
_usesDepthPrepass() {
// the resolve shades every pixel once, the large triangles of the hardware path are drawn directly
return false;
}
createView( width, height, occlusion ) {
if ( this._hardware === true ) {
const view = super.createView( width, height, occlusion );
if ( view !== null ) view.hardware = true;
return view;
}
const view = createView( width, height );
view.hardware = false;
const disposeBuffers = view.dispose;
view.occlusion = occlusion === true ? createOcclusion( width, height ) : null;
view.dispose = () => {
disposeBuffers();
if ( view.occlusion !== null ) view.occlusion.dispose();
};
return view;
}
/**
* The batches of the software rasterizer index the triangles of one geometry, each geometry has its own batches.
*
* @param {Object} clusters - The cluster data of the geometry.
* @return {Object} The key.
*/
_getBatchKey( clusters ) {
return clusters;
}
createBatch( entry, capacity, view, culling ) {
// the levels of detail whose triangles fit into the triangle index of the visibility buffer,
// the levels are ordered from the most detailed one
// the instances of instanced meshes are drawn as objects, their world matrices are computed on the GPU. the resolve
// doesn't read the instanced attributes of the geometry and the instance colors, these instanced meshes are drawn by the hardware rasterizer
// the software rasterizer doesn't evaluate the alpha of alpha-tested materials
if ( this._shadowPass === true || ( entry.instanced === true && ( entry.clusters.instancedAttributes.length > 0 || entry.instanceColor === true ) ) || entry.material.alphaTest > 0 || view === null || view.hardware === true ) return super.createBatch( entry, capacity, view, culling );
const lodTriangleEnds = entry.clusters.lodTriangleEnds;
let lodCount = 0;
while ( lodCount < lodTriangleEnds.length && lodTriangleEnds[ lodCount ] <= MAX_TRIANGLES ) lodCount ++;
if ( lodCount === 0 ) return super.createBatch( entry, capacity, view, culling );
const source = entry.source !== null ? entry.source.batch : null;
return new VisibilityBufferBatch( entry.clusters, entry.material, entry.receiveShadow, capacity, this.renderer, culling, { debug: this.debug, lodCount, view, instanced: entry.instanced, source, queueBudget: entry.queueBudget, maxChunks: entry.maxChunks, pool: this._geometryPool } );
}
}
export { VisibilityBufferDrawingPipeline };