UNPKG

@lume/three-instanced-mesh

Version:

Scene graph level abstraction for three.js InstancedBufferGeometry

434 lines (276 loc) 9.02 kB
import { monkeyPatch as _monkeyPatch} from './monkey-patch.js' export function monkeyPatch( THREE ){ const differentSignature = parseInt(THREE.REVISION) >= 96 //monkeypatch shaders _monkeyPatch(THREE); //depth mat var DEPTH_MATERIAL = new THREE.MeshDepthMaterial(); DEPTH_MATERIAL.depthPacking = THREE.RGBADepthPacking; DEPTH_MATERIAL.clipping = true; DEPTH_MATERIAL.defines = { INSTANCE_TRANSFORM: '' }; //distance mat var DISTANCE_SHADER = THREE.ShaderLib[ "distanceRGBA" ], DISTANCE_UNIFORMS = THREE.UniformsUtils.clone( DISTANCE_SHADER.uniforms ), DISTANCE_DEFINES = { 'USE_SHADOWMAP': '', 'INSTANCE_TRANSFORM': '' }, DISTANCE_MATERIAL = new THREE.ShaderMaterial( { defines: DISTANCE_DEFINES, uniforms: DISTANCE_UNIFORMS, vertexShader: DISTANCE_SHADER.vertexShader, fragmentShader: DISTANCE_SHADER.fragmentShader, clipping: true }) ; //main class function InstancedMesh( bufferGeometry, material, numInstances, dynamic, colors, uniformScale ) { THREE.Mesh.call( this , (new THREE.InstancedBufferGeometry()).copy( bufferGeometry ) ); //hacky for now this._dynamic = !!dynamic; //TODO: set a bit mask for different attributes? this._uniformScale = !!uniformScale; this._colors = !!colors; this.numInstances = numInstances; this._setAttributes(); /** * use the setter to decorate this material * this is in lieu of changing the renderer * WebGLRenderer injects stuff like this */ this.material = material.clone(); this.frustumCulled = false; //you can uncheck this if you generate your own bounding info //make it work with depth effects this.customDepthMaterial = DEPTH_MATERIAL; this.customDistanceMaterial = DISTANCE_MATERIAL; } InstancedMesh.prototype = Object.create( THREE.Mesh.prototype ); InstancedMesh.constructor = InstancedMesh; //this is kinda gnarly, done in order to avoid setting these defines in the WebGLRenderer (it manages most if not all of the define flags) Object.defineProperties( InstancedMesh.prototype , { 'material': { set: function( m ){ /** * whenever a material is set, decorate it, * if a material used with regular geometry is passed, * it will mutate it which is bad mkay * * either flag Material with these instance properties: * * "i want to create a RED PLASTIC material that will * be INSTANCED and i know it will be used on clones * that are known to be UNIFORMly scaled" * (also figure out where dynamic fits here) * * or check here if the material has INSTANCE_TRANSFORM * define set, if not, clone, document that it breaks reference * or do a shallow copy or something * * or something else? */ m = m.clone(); if ( m.defines ) { m.defines.INSTANCE_TRANSFORM = ''; if ( this._uniformScale ) m.defines.INSTANCE_UNIFORM = ''; //an optimization, should avoid doing an expensive matrix inverse in the shader else delete m.defines['INSTANCE_UNIFORM']; if ( this._colors ) m.defines.INSTANCE_COLOR = ''; else delete m.defines['INSTANCE_COLOR']; } else{ m.defines = { INSTANCE_TRANSFORM: '' }; if ( this._uniformScale ) m.defines.INSTANCE_UNIFORM = ''; if ( this._colors ) m.defines.INSTANCE_COLOR = ''; } this._material = m; }, get: function(){ return this._material; } }, //force new attributes to be created when set? 'numInstances': { set: function( v ){ this._numInstances = v; //reset buffers this._setAttributes(); }, get: function(){ return this._numInstances; } }, //do some auto-magic when BufferGeometry is set //TODO: account for Geometry, or change this approach completely 'geometry':{ set: function( g ){ //if its not already instanced attach buffers if ( !!g.attributes.instancePosition ) { this._geometry = new THREE.InstancedBufferGeometry(); this._setAttributes(); } else this._geometry = g; }, get: function(){ return this._geometry; } } }); InstancedMesh.prototype.setPositionAt = function( index , position ){ this.geometry.attributes.instancePosition.setXYZ( index , position.x , position.y , position.z ); }; InstancedMesh.prototype.setQuaternionAt = function ( index , quat ) { this.geometry.attributes.instanceQuaternion.setXYZW( index , quat.x , quat.y , quat.z , quat.w ); }; InstancedMesh.prototype.setScaleAt = function ( index , scale ) { this.geometry.attributes.instanceScale.setXYZ( index , scale.x , scale.y , scale.z ); }; InstancedMesh.prototype.setColorAt = function ( index , color ) { if( !this._colors ) { console.warn( 'InstancedMesh: color not enabled'); return; } this.geometry.attributes.instanceColor.setXYZ( index , Math.floor( color.r * 255 ), Math.floor( color.g * 255 ), Math.floor( color.b * 255 ) ); }; InstancedMesh.prototype.setOpacityAt = function ( index , opacity ) { if( !this._colors ) { console.warn( 'InstancedMesh: color not enabled'); return; } this.geometry.attributes.instanceOpacity.setX( index, opacity ); }; InstancedMesh.prototype.getPositionAt = function( index , position ){ var arr = this.geometry.attributes.instancePosition.array; index *= 3; return position ? position.set( arr[index++], arr[index++], arr[index] ) : new THREE.Vector3( arr[index++], arr[index++], arr[index] ) ; }; InstancedMesh.prototype.getQuaternionAt = function ( index , quat ) { var arr = this.geometry.attributes.instanceQuaternion.array; index = index << 2; return quat ? quat.set( arr[index++], arr[index++], arr[index++], arr[index] ) : new THREE.Quaternion( arr[index++], arr[index++], arr[index++], arr[index] ) ; }; InstancedMesh.prototype.getScaleAt = function ( index , scale ) { var arr = this.geometry.attributes.instanceScale.array; index *= 3; return scale ? scale.set( arr[index++], arr[index++], arr[index] ) : new THREE.Vector3( arr[index++], arr[index++], arr[index] ) ; }; InstancedMesh.prototype.getColorAt = (function(){ var inv255 = 1/255; return function ( index , color ) { if( !this._colors ) { console.warn( 'InstancedMesh: color not enabled'); return false; } var arr = this.geometry.attributes.instanceColor.array; index *= 3; return color ? color.setRGB( arr[index++] * inv255, arr[index++] * inv255, arr[index] * inv255 ) : new THREE.Vector3( arr[index++], arr[index++], arr[index] ).multiplyScalar( inv255 ) ; }; })() InstancedMesh.prototype.getOpacityAt = function ( index ) { if( !this._colors ) { console.warn( 'InstancedMesh: color not enabled'); return false; } return this.geometry.attributes.instanceOpacity.getX( index ); }; InstancedMesh.prototype.needsUpdate = function( attribute ){ switch ( attribute ){ case 'position' : this.geometry.attributes.instancePosition.needsUpdate = true; break; case 'quaternion' : this.geometry.attributes.instanceQuaternion.needsUpdate = true; break; case 'scale' : this.geometry.attributes.instanceScale.needsUpdate = true; break; case 'colors' : this.geometry.attributes.instanceColor.needsUpdate = true; break; case 'opacity' : this.geometry.attributes.instanceOpacity.needsUpdate = true; break; default: this.geometry.attributes.instancePosition.needsUpdate = true; this.geometry.attributes.instanceQuaternion.needsUpdate = true; this.geometry.attributes.instanceScale.needsUpdate = true; if(this._colors){ this.geometry.attributes.instanceColor.needsUpdate = true; this.geometry.attributes.instanceOpacity.needsUpdate = true; } break; } }; InstancedMesh.prototype._setAttributes = function(){ var normalized = true var meshPerAttribute = 1 var vec4Size = 4 var vec3Size = 3 var attributes = { instancePosition: [ new Float32Array( this.numInstances * vec3Size ), vec3Size, !normalized, meshPerAttribute, ], instanceQuaternion: [ new Float32Array( this.numInstances * vec4Size ), vec4Size, !normalized, meshPerAttribute, ], instanceScale: [ new Float32Array( this.numInstances * vec3Size ), vec3Size, !normalized, meshPerAttribute, ] } if ( this._colors ){ attributes.instanceColor = [ new Uint8Array( this.numInstances * vec3Size ), vec3Size, normalized, meshPerAttribute, ], attributes.instanceOpacity = [ new Float32Array( this.numInstances ).fill( 1 ), 1, normalized, meshPerAttribute, ] } Object.keys(attributes).forEach(name=>{ const a = attributes[name] let attribute if(differentSignature){ attribute = new THREE.InstancedBufferAttribute(...a) } else { attribute = new THREE.InstancedBufferAttribute(a[0],a[1],a[3]) attribute.normalized = a[2] } attribute.dynamic = this._dynamic if ( THREE.REVISION >= 110 ) this.geometry.setAttribute(name, attribute) else this.geometry.addAttribute(name, attribute) }) }; return InstancedMesh; };