three-instanced-mesh
three-instanced-mesh copied to clipboard
Automatically set needsUpdate with correct updateRange
Currently using setPositon requires manually executing needsUpdate(). It would be more performant to narrow down only the needed positions that require updating by utilising updateRange and this could be calculated automatically when setting positions before each render
Can you make an example of how this would work. I'm actually not that familiar with update range, i'd have to research it a bit. How would it work if you don't have a sparse list of instances?
When setting needsUpdate, updateRange is used to limit the update only to given range of elements. i.e.
there are 50 instances and when you update instance 31 you don't want to update them all just instance 31 so you set update range { offset: 30 * attribute.itemSize, count: 1 * attribute.itemSize }
Here's the implementation that works in my project. btw thanks for making this library and it helps with instancing a lot! I'm good in JS but a beginner/semi-intermediate with shaders. I'm using it to draw tire tracks in my game engine http://bad.city
InstancedMesh.prototype.setUpdateRange = function (attribute, instanceNumber) {
const count = 1;
const start = instanceNumber;
if (!attribute.needsUpdate) {
attribute.updateRange.offset = start * attribute.itemSize;
attribute.updateRange.count = count * attribute.itemSize;
} else {
// if there's already an element to udpate increase the updateRange to contain from first to last one
const firstStart = Math.min(start, attribute.updateRange.offset);
const lastStart = Math.max(start, attribute.updateRange.offset, attribute.updateRange.offset + attribute.updateRange.count - 1);
attribute.updateRange.offset = firstStart * attribute.itemSize;
attribute.updateRange.count = (lastStart - firstStart + 1) * attribute.itemSize;
}
}
InstancedMesh.prototype.needsUpdate = function( attribute, instanceNumber ){
switch ( attribute ){
case 'position' :
this.setUpdateRange(this.geometry.attributes.instancePosition, instanceNumber);
this.geometry.attributes.instancePosition.needsUpdate = true;
break;
case 'quaternion' :
this.setUpdateRange(this.geometry.attributes.instanceQuaternion, instanceNumber);
this.geometry.attributes.instanceQuaternion.needsUpdate = true;
break;
case 'scale' :
this.setUpdateRange(this.geometry.attributes.instanceScale, instanceNumber);
this.geometry.attributes.instanceScale.needsUpdate = true;
break;
case 'colors' :
this.setUpdateRange(this.geometry.attributes.instanceColor, instanceNumber);
this.geometry.attributes.instanceColor.needsUpdate = true;
break;
default:
this.setUpdateRange(this.geometry.attributes.instancePosition, instanceNumber);
this.setUpdateRange(this.geometry.attributes.instanceQuaternion, instanceNumber);
this.setUpdateRange(this.geometry.attributes.instanceScale, instanceNumber);
this.geometry.attributes.instancePosition.needsUpdate = true;
this.geometry.attributes.instanceQuaternion.needsUpdate = true;
this.geometry.attributes.instanceScale.needsUpdate = true;
if(this._colors){
this.setUpdateRange(this.geometry.attributes.instanceColor, instanceNumber);
this.geometry.attributes.instanceColor.needsUpdate = true;
}
break;
}
};
I found a problem when setting an updateRange multiple times before the renderer sends the changes to the GPU. In the code below all the fixes are contained to not loose any update ranges even if they are done multiple times before rendering. It's using onAfterRender to flag if an attribute updateRange should be extended or if it's just after the render it can be completely overwritten.
I also added support for objects with multiple materials. Surprisingly it worked! Now entire wheels with 3 materials each are instances in my engine. So 4 wheels x 3 draw calls = 12 is now reduced to 3 draw calls. Nice.
Probably I should make a PR but I'm not sure if you want to convert this project to ES6 (I'm using individual imports instead of i.e. THREE.BuffferGeometry).
import { MeshDepthMaterial, RGBADepthPacking, ShaderLib, UniformsUtils, ShaderMaterial, Mesh, InstancedBufferGeometry, Vector3, Quaternion, InstancedBufferAttribute } from '../js/_THREE';
import './InstancedMeshPatch/index';
/**************************
* Dusan Bosnjak @pailhead
**************************/
//depth mat
var DEPTH_MATERIAL = new MeshDepthMaterial();
DEPTH_MATERIAL.depthPacking = RGBADepthPacking;
DEPTH_MATERIAL.clipping = true;
DEPTH_MATERIAL.defines = {
'INSTANCE_TRANSFORM': ''
};
//distance mat
var DISTANCE_SHADER = ShaderLib[ "distanceRGBA" ],
DISTANCE_UNIFORMS = UniformsUtils.clone( DISTANCE_SHADER.uniforms ),
DISTANCE_DEFINES = {
'USE_SHADOWMAP': '',
'INSTANCE_TRANSFORM': ''
},
DISTANCE_MATERIAL = new ShaderMaterial( {
defines: DISTANCE_DEFINES,
uniforms: DISTANCE_UNIFORMS,
vertexShader: DISTANCE_SHADER.vertexShader,
fragmentShader: DISTANCE_SHADER.fragmentShader,
clipping: true
})
;
//main class
export 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 = Array.isArray(material) ? material.map(mat => mat.clone()) : material.clone();
this.frustumCulled = false; //you can uncheck this if you generate your own bounding info
this.lastKnownVersions = {
'instancePosition': 0,
'instanceQuaternion': 0,
'instanceScale': 0,
'instanceColor': 0,
};
//make it work with depth effects
this.customDepthMaterial = DEPTH_MATERIAL;
this.customDistanceMaterial = DISTANCE_MATERIAL;
}
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?
*/
const isArray = Array.isArray(m);
const mats = isArray ? m.map(mat => mat.clone()) : [m.clone()];
mats.forEach(m => {
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 = isArray ? mats : mats[0];
},
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( '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( '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.onAfterRender = function() {
if (this.geometry.attributes.instancePosition) {
this.lastKnownVersions[this.geometry.attributes.instancePosition.name] = this.geometry.attributes.instancePosition.version;
}
if (this.geometry.attributes.instanceQuaternion) {
this.lastKnownVersions[this.geometry.attributes.instanceQuaternion.name] = this.geometry.attributes.instanceQuaternion.version;
}
if (this.geometry.attributes.instanceScale) {
this.lastKnownVersions[this.geometry.attributes.instanceScale.name] = this.geometry.attributes.instanceScale.version;
}
if (this.geometry.attributes.instanceColor) {
this.lastKnownVersions[this.geometry.attributes.instanceColor.name] = this.geometry.attributes.instanceColor.version;
}
};
InstancedMesh.prototype.setUpdateRange = function (attribute, instanceFrom, instanceTo) {
// no params given, update all
if (instanceFrom === undefined) {
attribute.updateRange.offset = 0;
attribute.updateRange.count = -1;
return;
}
// only instanceFrom given - automatically update including all changes before renderer updated attribute's version
const start = instanceFrom * attribute.itemSize;
const count = (instanceTo || 1) * attribute.itemSize;
if (this.lastKnownVersions[attribute.name] === attribute.version) {
attribute.updateRange.offset = start;
attribute.updateRange.count = count;
} else {
// if there's already an element to udpate increase the updateRange to contain from first to last one
const rangeStart = Math.min(start, attribute.updateRange.offset);
const rangeEnd = Math.max(start + count, attribute.updateRange.offset + attribute.updateRange.count);
attribute.updateRange.offset = rangeStart;
attribute.updateRange.count = rangeEnd - rangeStart;
}
}
InstancedMesh.prototype.needsUpdate = function( attribute, instanceFrom, instanceTo ){
switch ( attribute ){
case 'position' :
this.setUpdateRange(this.geometry.attributes.instancePosition, instanceFrom, instanceTo);
this.geometry.attributes.instancePosition.needsUpdate = true;
break;
case 'quaternion' :
this.setUpdateRange(this.geometry.attributes.instanceQuaternion, instanceFrom, instanceTo);
this.geometry.attributes.instanceQuaternion.needsUpdate = true;
break;
case 'scale' :
this.setUpdateRange(this.geometry.attributes.instanceScale, instanceFrom, instanceTo);
this.geometry.attributes.instanceScale.needsUpdate = true;
break;
case 'colors' :
this.setUpdateRange(this.geometry.attributes.instanceColor, instanceFrom, instanceTo);
this.geometry.attributes.instanceColor.needsUpdate = true;
break;
default:
this.setUpdateRange(this.geometry.attributes.instancePosition, instanceFrom, instanceTo);
this.setUpdateRange(this.geometry.attributes.instanceQuaternion, instanceFrom, instanceTo);
this.setUpdateRange(this.geometry.attributes.instanceScale, instanceFrom, instanceTo);
this.geometry.attributes.instancePosition.needsUpdate = true;
this.geometry.attributes.instanceQuaternion.needsUpdate = true;
this.geometry.attributes.instanceScale.needsUpdate = true;
if(this._colors){
this.setUpdateRange(this.geometry.attributes.instanceColor, instanceFrom, instanceTo);
this.geometry.attributes.instanceColor.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,
]
}
Object.keys(attributes).forEach(name=>{
const a = attributes[name];
// for version above 96
// const attribute = new InstancedBufferAttribute(...a);
const attribute = new InstancedBufferAttribute(a[0],a[1],a[3]);
attribute.normalized = a[2];
attribute.name = name;
attribute.dynamic = this._dynamic;
this.geometry.addAttribute(name, attribute)
})
};
@DVLP
Do you have time to maybe discuss this over slack or something?
I'm thinking of some kind of autoUpdate flag, either depending on the scenegraph auto-updating or its own. I'm not sure what to do when you update sparse instances. Say if you call set on instances 0-15 individually, before you update, you could have one range from 0-15. But as soon as you update 20, you have another range 20-20. Worse case scenario odds would be updated evens would not. What would be a sensible way to distinguish if it would be faster updating several ranges or just the whole thing?