UNPKG

three

Version:

JavaScript 3D library

479 lines (307 loc) 13.8 kB
import { Box3, Color, Matrix4, Vector2, Vector3, BackSide, DoubleSide, SRGBColorSpace } from 'three/webgpu'; const _matrix = /*@__PURE__*/ new Matrix4(); const _box = /*@__PURE__*/ new Box3(); const _va = /*@__PURE__*/ new Vector3(); const _vb = /*@__PURE__*/ new Vector3(); const _vc = /*@__PURE__*/ new Vector3(); const _uva = /*@__PURE__*/ new Vector2(); const _uvb = /*@__PURE__*/ new Vector2(); const _uvc = /*@__PURE__*/ new Vector2(); const _uv = /*@__PURE__*/ new Vector2(); const _color = /*@__PURE__*/ new Color(); const _albedo = /*@__PURE__*/ new Color(); const _emissive = /*@__PURE__*/ new Color(); const IMAGE_SAMPLE_SIZE = 64; /** * Number of floats per triangle record: three positions (vec4 each, w unused), * albedo (rgb + side flag) and emissive (rgb + unused). */ export const TRIANGLE_STRIDE = 20; const _srgbLUT = new Float32Array( 256 ); for ( let i = 0; i < 256; i ++ ) { const c = i / 255; _srgbLUT[ i ] = ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 ); } class GrowableFloat32Array { constructor( initialSize ) { this.array = new Float32Array( initialSize ); this.length = 0; } ensure( count ) { if ( this.length + count > this.array.length ) { let size = this.array.length * 2; while ( size < this.length + count ) size *= 2; const array = new Float32Array( size ); array.set( this.array ); this.array = array; } } push4( x, y, z, w ) { const a = this.array, i = this.length; a[ i ] = x; a[ i + 1 ] = y; a[ i + 2 ] = z; a[ i + 3 ] = w; this.length += 4; } result() { return this.array.slice( 0, this.length ); } } /** * Downscales a texture image into a small pixel buffer so triangle colors can be * looked up on the CPU. Returns `null` for images that cannot be read. * * @private * @param {Texture} texture - The texture. * @param {Map<string, ?Object>} cache - Cache keyed by texture uuid. * @return {?{data: Uint8ClampedArray, width: number, height: number, srgb: boolean}} The image sampler. */ function getImageSampler( texture, cache ) { if ( cache.has( texture.uuid ) ) return cache.get( texture.uuid ); let sampler = null; const image = texture.image; if ( image && texture.isCompressedTexture !== true ) { const srgb = texture.colorSpace === SRGBColorSpace; if ( image.data !== undefined && image.width !== undefined ) { // DataTexture (RGBA 8-bit only) if ( image.data.length === image.width * image.height * 4 && image.data.BYTES_PER_ELEMENT === 1 ) { sampler = { data: image.data, width: image.width, height: image.height, srgb }; } } else if ( image.width > 0 && image.height > 0 ) { try { const size = IMAGE_SAMPLE_SIZE; const canvas = ( typeof OffscreenCanvas !== 'undefined' ) ? new OffscreenCanvas( size, size ) : document.createElement( 'canvas' ); canvas.width = size; canvas.height = size; const context = canvas.getContext( '2d', { willReadFrequently: true } ); context.imageSmoothingEnabled = true; context.imageSmoothingQuality = 'high'; context.drawImage( image, 0, 0, size, size ); sampler = { data: context.getImageData( 0, 0, size, size ).data, width: size, height: size, srgb }; } catch ( e ) { sampler = null; } } } cache.set( texture.uuid, sampler ); return sampler; } /** * Samples the given texture at the given uv coordinates. The result is written * into `target` (linear rgb) and the alpha is returned. * * @private */ function sampleTexture( texture, sampler, uv, target ) { _uv.copy( uv ); texture.transformUv( _uv ); const x = Math.min( sampler.width - 1, Math.max( 0, Math.floor( _uv.x * sampler.width ) ) ); const y = Math.min( sampler.height - 1, Math.max( 0, Math.floor( _uv.y * sampler.height ) ) ); const i = ( y * sampler.width + x ) * 4; const data = sampler.data; if ( sampler.srgb ) { target.setRGB( _srgbLUT[ data[ i ] ], _srgbLUT[ data[ i + 1 ] ], _srgbLUT[ data[ i + 2 ] ] ); } else { target.setRGB( data[ i ] / 255, data[ i + 1 ] / 255, data[ i + 2 ] / 255 ); } return data[ i + 3 ] / 255; } function getMaterialInfo( material, cache, imageCache ) { if ( cache.has( material ) ) return cache.get( material ); const unlit = material.isMeshBasicMaterial === true || material.isMeshBasicNodeMaterial === true; const info = { unlit, side: material.side === BackSide ? 1 : ( material.side === DoubleSide ? 2 : 0 ), opacity: material.transparent === true ? material.opacity : 1, alphaTest: material.alphaTest || 0, color: material.color ? material.color : null, emissive: ( ! unlit && material.emissive ) ? material.emissive : null, emissiveIntensity: material.emissiveIntensity !== undefined ? material.emissiveIntensity : 1, map: null, mapSampler: null, emissiveMap: null, emissiveMapSampler: null }; if ( material.map && material.map.isTexture ) { info.mapSampler = getImageSampler( material.map, imageCache ); if ( info.mapSampler !== null ) info.map = material.map; } if ( ! unlit && material.emissiveMap && material.emissiveMap.isTexture ) { info.emissiveMapSampler = getImageSampler( material.emissiveMap, imageCache ); if ( info.emissiveMapSampler !== null ) info.emissiveMap = material.emissiveMap; } // the uv transform is normally updated by the renderer if ( info.map !== null && info.map.matrixAutoUpdate === true ) info.map.updateMatrix(); if ( info.emissiveMap !== null && info.emissiveMap.matrixAutoUpdate === true ) info.emissiveMap.updateMatrix(); cache.set( material, info ); return info; } /** * Returns `true` if the object should contribute geometry to the voxel volume. * * @private */ function isVoxelizable( object, layers, exclude ) { return object.isMesh === true && object.isSkyMesh !== true && object.isBatchedMesh !== true && layers.test( object.layers ) === true && exclude.has( object ) === false && object.geometry !== undefined; } /** * Computes the world-space bounding box of all voxelizable meshes. * * @param {Scene} scene - The scene. * @param {Layers} layers - Only objects that pass the layer test are considered. * @param {Set<Object3D>} exclude - Objects to skip. * @param {Box3} target - The target box. * @return {Box3} The bounding box. */ export function computeSceneBounds( scene, layers, exclude, target ) { target.makeEmpty(); scene.traverseVisible( ( object ) => { if ( isVoxelizable( object, layers, exclude ) ) { target.expandByObject( object, true ); } } ); return target; } /** * Collects the triangles of all meshes in the scene as flat, world-space records suitable * for GPU voxelization. Per-triangle albedo and emissive colors are resolved on the CPU * (material color multiplied with a texture lookup at the triangle's centroid). Large * triangles are subdivided so every record covers a bounded number of voxels. * * @param {Scene} scene - The scene. * @param {Object} options - Options. * @param {Box3} options.bounds - Triangles outside these bounds are skipped. * @param {Layers} options.layers - Objects must pass this layer test. * @param {Set<Object3D>} options.exclude - Objects to skip. * @param {number} options.subVoxelSize - Size of a sub-voxel in world units. * @param {number} options.maxEdge - Maximum triangle edge length in sub-voxels before subdivision. * @param {number} options.minOpacity - Triangles with lower opacity are skipped. * @return {{data: Float32Array, count: number}} The triangle records and triangle count. */ export function collectSceneTriangles( scene, options ) { const { bounds, layers, exclude, subVoxelSize, maxEdge, minOpacity } = options; const materialCache = new Map(); const imageCache = new Map(); const buffer = new GrowableFloat32Array( 1024 * TRIANGLE_STRIDE ); const maxEdgeSq = ( maxEdge * subVoxelSize ) * ( maxEdge * subVoxelSize ); // triangle emission with subdivision of large triangles const stack = []; function emit( a, b, c, albedo, emissive, side ) { stack.push( a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z ); while ( stack.length > 0 ) { const cz = stack.pop(), cy = stack.pop(), cx = stack.pop(); const bz = stack.pop(), by = stack.pop(), bx = stack.pop(); const az = stack.pop(), ay = stack.pop(), ax = stack.pop(); const abSq = ( bx - ax ) ** 2 + ( by - ay ) ** 2 + ( bz - az ) ** 2; const bcSq = ( cx - bx ) ** 2 + ( cy - by ) ** 2 + ( cz - bz ) ** 2; const caSq = ( ax - cx ) ** 2 + ( ay - cy ) ** 2 + ( az - cz ) ** 2; const longest = Math.max( abSq, bcSq, caSq ); if ( longest > maxEdgeSq ) { // split along the longest edge if ( longest === abSq ) { const mx = ( ax + bx ) * 0.5, my = ( ay + by ) * 0.5, mz = ( az + bz ) * 0.5; stack.push( ax, ay, az, mx, my, mz, cx, cy, cz ); stack.push( mx, my, mz, bx, by, bz, cx, cy, cz ); } else if ( longest === bcSq ) { const mx = ( bx + cx ) * 0.5, my = ( by + cy ) * 0.5, mz = ( bz + cz ) * 0.5; stack.push( ax, ay, az, bx, by, bz, mx, my, mz ); stack.push( ax, ay, az, mx, my, mz, cx, cy, cz ); } else { const mx = ( cx + ax ) * 0.5, my = ( cy + ay ) * 0.5, mz = ( cz + az ) * 0.5; stack.push( ax, ay, az, bx, by, bz, mx, my, mz ); stack.push( mx, my, mz, bx, by, bz, cx, cy, cz ); } continue; } buffer.ensure( TRIANGLE_STRIDE ); buffer.push4( ax, ay, az, 0 ); buffer.push4( bx, by, bz, 0 ); buffer.push4( cx, cy, cz, 0 ); buffer.push4( albedo.r, albedo.g, albedo.b, side ); buffer.push4( emissive.r, emissive.g, emissive.b, 0 ); } } function processMesh( mesh, matrixWorld ) { const geometry = mesh.geometry; const positionAttribute = geometry.getAttribute( 'position' ); if ( positionAttribute === undefined ) return; const index = geometry.index; const materials = Array.isArray( mesh.material ) ? mesh.material : [ mesh.material ]; const vertexCount = index !== null ? index.count : positionAttribute.count; let groups = geometry.groups; if ( Array.isArray( mesh.material ) === false || groups.length === 0 ) { groups = [ { start: 0, count: vertexCount, materialIndex: 0 } ]; } const drawRange = geometry.drawRange; const rangeStart = drawRange.start; const rangeEnd = Math.min( vertexCount, drawRange.start + drawRange.count ); for ( const group of groups ) { const material = materials[ group.materialIndex ]; if ( material === undefined || material === null || material.visible === false ) continue; const info = getMaterialInfo( material, materialCache, imageCache ); if ( info.opacity < minOpacity ) continue; const uvAttribute = info.map !== null ? geometry.getAttribute( info.map.channel > 0 ? 'uv' + info.map.channel : 'uv' ) : null; const emissiveUVAttribute = info.emissiveMap !== null ? geometry.getAttribute( info.emissiveMap.channel > 0 ? 'uv' + info.emissiveMap.channel : 'uv' ) : null; const start = Math.max( group.start, rangeStart ); const end = Math.min( group.start + group.count, rangeEnd ); for ( let i = start; i + 2 < end; i += 3 ) { const ia = index !== null ? index.getX( i ) : i; const ib = index !== null ? index.getX( i + 1 ) : i + 1; const ic = index !== null ? index.getX( i + 2 ) : i + 2; _va.fromBufferAttribute( positionAttribute, ia ).applyMatrix4( matrixWorld ); _vb.fromBufferAttribute( positionAttribute, ib ).applyMatrix4( matrixWorld ); _vc.fromBufferAttribute( positionAttribute, ic ).applyMatrix4( matrixWorld ); // skip triangles outside the volume _box.makeEmpty().expandByPoint( _va ).expandByPoint( _vb ).expandByPoint( _vc ); if ( bounds.intersectsBox( _box ) === false ) continue; // skip degenerate triangles if ( _box.min.distanceToSquared( _box.max ) < 1e-14 ) continue; // resolve colors let alpha = info.opacity; if ( info.color !== null ) _albedo.copy( info.color ); else _albedo.setRGB( 1, 1, 1 ); if ( uvAttribute !== undefined && uvAttribute !== null ) { _uva.fromBufferAttribute( uvAttribute, ia ); _uvb.fromBufferAttribute( uvAttribute, ib ); _uvc.fromBufferAttribute( uvAttribute, ic ); _uva.add( _uvb ).add( _uvc ).multiplyScalar( 1 / 3 ); alpha *= sampleTexture( info.map, info.mapSampler, _uva, _color ); _albedo.multiply( _color ); } if ( info.alphaTest > 0 && alpha < info.alphaTest ) continue; if ( alpha < minOpacity ) continue; if ( info.unlit ) { _emissive.copy( _albedo ); _albedo.setRGB( 0, 0, 0 ); } else { if ( info.emissive !== null ) { _emissive.copy( info.emissive ).multiplyScalar( info.emissiveIntensity ); if ( emissiveUVAttribute !== undefined && emissiveUVAttribute !== null ) { _uva.fromBufferAttribute( emissiveUVAttribute, ia ); _uvb.fromBufferAttribute( emissiveUVAttribute, ib ); _uvc.fromBufferAttribute( emissiveUVAttribute, ic ); _uva.add( _uvb ).add( _uvc ).multiplyScalar( 1 / 3 ); sampleTexture( info.emissiveMap, info.emissiveMapSampler, _uva, _color ); _emissive.multiply( _color ); } } else { _emissive.setRGB( 0, 0, 0 ); } } emit( _va, _vb, _vc, _albedo, _emissive, info.side ); } } } scene.traverseVisible( ( object ) => { if ( isVoxelizable( object, layers, exclude ) === false ) return; if ( object.isInstancedMesh === true ) { for ( let i = 0; i < object.count; i ++ ) { object.getMatrixAt( i, _matrix ); _matrix.premultiply( object.matrixWorld ); processMesh( object, _matrix ); } } else { processMesh( object, object.matrixWorld ); } } ); const data = buffer.result(); return { data, count: data.length / TRIANGLE_STRIDE }; }