diff --git a/examples/files.js b/examples/files.js index 15fff9e71ddecf..dca4335e6f29e2 100644 --- a/examples/files.js +++ b/examples/files.js @@ -1,5 +1,7 @@ var files = { "webgl": [ + + "webgl_instanced_mesh", "webgl_animation_cloth", "webgl_animation_keyframes_json", "webgl_animation_scene", diff --git a/examples/webgl_instanced_mesh.html b/examples/webgl_instanced_mesh.html new file mode 100644 index 00000000000000..cb64dbc52a10b5 --- /dev/null +++ b/examples/webgl_instanced_mesh.html @@ -0,0 +1,463 @@ + + + + three.js webgl - InstancedMesh example + + + + + +
+ three.js - Mesh abstraction for InstancedBufferGeometry pailhead +
+ + + + + + + + + + + + + + diff --git a/src/Three.js b/src/Three.js index 86c86576d9b359..2a0b598da327ad 100644 --- a/src/Three.js +++ b/src/Three.js @@ -18,6 +18,7 @@ export { SkinnedMesh } from './objects/SkinnedMesh.js'; export { Skeleton } from './objects/Skeleton.js'; export { Bone } from './objects/Bone.js'; export { Mesh } from './objects/Mesh.js'; +export { InstancedMesh } from './objects/InstancedMesh.js'; export { LineSegments } from './objects/LineSegments.js'; export { LineLoop } from './objects/LineLoop.js'; export { Line } from './objects/Line.js'; diff --git a/src/objects/InstancedMesh.js b/src/objects/InstancedMesh.js new file mode 100644 index 00000000000000..99266dbdaf9ecc --- /dev/null +++ b/src/objects/InstancedMesh.js @@ -0,0 +1,374 @@ +/** + * @author pailhead / www.dusanbosnjak.com + */ + +import { Mesh } from './Mesh'; +import { Object3D } from '../core/Object3D'; +import { InstancedBufferGeometry } from '../core/InstancedBufferGeometry'; +import { InstancedBufferAttribute } from '../core/InstancedBufferAttribute'; +import { Matrix3 } from '../math/Matrix3'; +import { Matrix4 } from '../math/Matrix4'; +import { Vector3 } from '../math/Vector3'; +import { Quaternion } from '../math/Quaternion'; + +import { MeshDepthMaterial } from '../materials/MeshDepthMaterial'; +import { RGBADepthPacking } from '../constants'; +import { ShaderMaterial } from '../materials/ShaderMaterial'; +import { UniformsUtils } from '../renderers/shaders/UniformsUtils'; +import { ShaderLib } from '../renderers/shaders/ShaderLib'; + + +//custom depth and distance material to be attached to meshes + +var depthMaterialTemplate = new MeshDepthMaterial(); + +depthMaterialTemplate.depthPacking = RGBADepthPacking; + +depthMaterialTemplate.clipping = true; + +depthMaterialTemplate.defines = { + + INSTANCE_TRANSFORM: '' + +}; + +var + + distanceShader = ShaderLib[ "distanceRGBA" ], + distanceUniforms = UniformsUtils.clone( distanceShader.uniforms ), + distanceMaterialTemplate = new ShaderMaterial( { + defines: { + 'USE_SHADOWMAP': '', + 'INSTANCE_TRANSFORM': '' + }, + uniforms: distanceUniforms, + vertexShader: distanceShader.vertexShader, + fragmentShader: distanceShader.fragmentShader, + clipping: true + }) +; + +//main class +function InstancedMesh ( bufferGeometry , material , numInstances , dynamic , colors , uniformScale ) { + + Mesh.call( this , (new 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 = depthMaterialTemplate; + + this.customDistanceMaterial = distanceMaterialTemplate; + +} + +InstancedMesh.prototype = Object.create( 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 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( 'THREE.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.setColorAt = function ( index , color ) { + + if( !this._colors ) { + + console.warn( 'THREE.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.getPositionAt = function( index , position ){ + + var arr = this.geometry.attributes.instancePosition.array; + + index *= 3; + + return position ? + + position.set( arr[index++], arr[index++], arr[index] ) : + + new 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 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 Vector3( arr[index++], arr[index++], arr[index] ) + ; + +}; + +InstancedMesh.prototype.getColorAt = (function(){ + + var inv255 = 1/255; + + return function ( index , color ) { + + if( !this._colors ) { + + console.warn( 'THREE.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 Vector3( arr[index++], arr[index++], arr[index] ).multiplyScalar( inv255 ) + ; + + }; + +})() + +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; + + default: + + this.geometry.attributes.instancePosition.needsUpdate = true; + this.geometry.attributes.instanceQuaternion.needsUpdate = true; + this.geometry.attributes.instanceScale.needsUpdate = true; + this.geometry.attributes.instanceColor.needsUpdate = true; + + break; + + } + +}; + +InstancedMesh.prototype._setAttributes = function(){ + + this.geometry.addAttribute( 'instancePosition' , new InstancedBufferAttribute( new Float32Array( this.numInstances * 3 ) , 3 , 1 ) ); + this.geometry.addAttribute( 'instanceQuaternion' , new InstancedBufferAttribute( new Float32Array( this.numInstances * 4 ) , 4 , 1 ) ); + this.geometry.addAttribute( 'instanceScale' , new InstancedBufferAttribute( new Float32Array( this.numInstances * 3 ) , 3 , 1 ) ); + + //TODO: allow different combinations + this.geometry.attributes.instancePosition.dynamic = this._dynamic; + this.geometry.attributes.instanceQuaternion.dynamic = this._dynamic; + this.geometry.attributes.instanceScale.dynamic = this._dynamic; + + if ( this._colors ){ + + this.geometry.addAttribute( 'instanceColor' , new InstancedBufferAttribute( new Uint8Array( this.numInstances * 3 ) , 3 , 1 ) ); + this.geometry.attributes.instanceColor.normalized = true; + this.geometry.attributes.instanceColor.dynamic = this._dynamic; + + } + +}; + +export { InstancedMesh }; \ No newline at end of file diff --git a/src/renderers/shaders/ShaderChunk/begin_vertex.glsl b/src/renderers/shaders/ShaderChunk/begin_vertex.glsl index 9f681c6600faa8..29c32b1830afc1 100644 --- a/src/renderers/shaders/ShaderChunk/begin_vertex.glsl +++ b/src/renderers/shaders/ShaderChunk/begin_vertex.glsl @@ -1,2 +1,19 @@ +#ifndef INSTANCE_TRANSFORM + vec3 transformed = vec3( position ); + +#else + +#ifndef INSTANCE_MATRIX + + mat4 _instanceMatrix = getInstanceMatrix(); + + #define INSTANCE_MATRIX + +#endif + +vec3 transformed = ( _instanceMatrix * vec4( position , 1. )).xyz; + +#endif + diff --git a/src/renderers/shaders/ShaderChunk/color_fragment.glsl b/src/renderers/shaders/ShaderChunk/color_fragment.glsl index 693fdf061c226f..31ff04baedcc52 100644 --- a/src/renderers/shaders/ShaderChunk/color_fragment.glsl +++ b/src/renderers/shaders/ShaderChunk/color_fragment.glsl @@ -2,4 +2,10 @@ diffuseColor.rgb *= vColor; +#endif + +#ifdef INSTANCE_COLOR + + diffuseColor.rgb *= vInstanceColor; + #endif \ No newline at end of file diff --git a/src/renderers/shaders/ShaderChunk/color_pars_fragment.glsl b/src/renderers/shaders/ShaderChunk/color_pars_fragment.glsl index 4f59898b6ef7fd..6dfe8262779e70 100644 --- a/src/renderers/shaders/ShaderChunk/color_pars_fragment.glsl +++ b/src/renderers/shaders/ShaderChunk/color_pars_fragment.glsl @@ -3,3 +3,9 @@ varying vec3 vColor; #endif + +#if defined( INSTANCE_COLOR ) + + varying vec3 vInstanceColor; + +#endif diff --git a/src/renderers/shaders/ShaderChunk/color_vertex.glsl b/src/renderers/shaders/ShaderChunk/color_vertex.glsl index 7bd534d9cf170f..802e9df44bd5d9 100644 --- a/src/renderers/shaders/ShaderChunk/color_vertex.glsl +++ b/src/renderers/shaders/ShaderChunk/color_vertex.glsl @@ -2,4 +2,10 @@ vColor.xyz = color.xyz; +#endif + +#if defined( INSTANCE_COLOR ) && defined( INSTANCE_TRANSFORM ) + + vInstanceColor = instanceColor; + #endif \ No newline at end of file diff --git a/src/renderers/shaders/ShaderChunk/defaultnormal_vertex.glsl b/src/renderers/shaders/ShaderChunk/defaultnormal_vertex.glsl index d9fb04ee0a892a..f0150d478a10a8 100644 --- a/src/renderers/shaders/ShaderChunk/defaultnormal_vertex.glsl +++ b/src/renderers/shaders/ShaderChunk/defaultnormal_vertex.glsl @@ -4,4 +4,30 @@ #endif +#ifndef INSTANCE_TRANSFORM + vec3 transformedNormal = normalMatrix * objectNormal; + +#else + + + +#ifndef INSTANCE_MATRIX + + mat4 _instanceMatrix = getInstanceMatrix(); + + #define INSTANCE_MATRIX + +#endif + +#ifndef INSTANCE_UNIFORM + +vec3 transformedNormal = transpose( inverse( mat3( modelViewMatrix * _instanceMatrix ) ) ) * objectNormal ; + +#else + +vec3 transformedNormal = ( modelViewMatrix * _instanceMatrix * vec4( objectNormal , 0.0 ) ).xyz; + +#endif + +#endif \ No newline at end of file diff --git a/src/renderers/shaders/ShaderChunk/uv_pars_vertex.glsl b/src/renderers/shaders/ShaderChunk/uv_pars_vertex.glsl index d2e81dd526c1cc..808c970ff27cde 100644 --- a/src/renderers/shaders/ShaderChunk/uv_pars_vertex.glsl +++ b/src/renderers/shaders/ShaderChunk/uv_pars_vertex.glsl @@ -4,3 +4,61 @@ uniform vec4 offsetRepeat; #endif + +//@author pailhead +//for now the most convenient place to attach vert transformation logic in global scope ( before main() ) +#if defined( INSTANCE_TRANSFORM ) + +mat3 inverse(mat3 m) { + float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; + float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2]; + float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2]; + + float b01 = a22 * a11 - a12 * a21; + float b11 = -a22 * a10 + a12 * a20; + float b21 = a21 * a10 - a11 * a20; + + float det = a00 * b01 + a01 * b11 + a02 * b21; + + return mat3(b01, (-a22 * a01 + a02 * a21), (a12 * a01 - a02 * a11), + b11, (a22 * a00 - a02 * a20), (-a12 * a00 + a02 * a10), + b21, (-a21 * a00 + a01 * a20), (a11 * a00 - a01 * a10)) / det; +} + +//for dynamic, avoid computing the matrices on the cpu +attribute vec3 instancePosition; +attribute vec4 instanceQuaternion; +attribute vec3 instanceScale; + +#if defined( INSTANCE_COLOR ) + attribute vec3 instanceColor; + varying vec3 vInstanceColor; +#endif + +mat4 getInstanceMatrix(){ + + vec4 q = instanceQuaternion; + vec3 s = instanceScale; + vec3 v = instancePosition; + + vec3 q2 = q.xyz + q.xyz; + vec3 a = q.xxx * q2.xyz; + vec3 b = q.yyz * q2.yzz; + vec3 c = q.www * q2.xyz; + + vec3 r0 = vec3( 1.0 - (b.x + b.z) , a.y + c.z , a.z - c.y ) * s.xxx; + vec3 r1 = vec3( a.y - c.z , 1.0 - (a.x + b.z) , b.y + c.x ) * s.yyy; + vec3 r2 = vec3( a.z + c.y , b.y - c.x , 1.0 - (a.x + b.x) ) * s.zzz; + + return mat4( + + r0 , 0., + r1 , 0., + r2 , 0., + v , 1.0 + + ); + +} + +#endif