Skip to content
1 change: 1 addition & 0 deletions examples/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var files = {
"webgl_geometry_text_shapes",
"webgl_geometry_text_stroke",
"webgl_helpers",
"webgl_instancing",
"webgl_interactive_buffergeometry",
"webgl_interactive_cubes",
"webgl_interactive_cubes_gpu",
Expand Down
132 changes: 132 additions & 0 deletions examples/webgl_instancing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - instancing</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>

<script type="module">

import * as THREE from '../build/three.module.js';

import Stats from './jsm/libs/stats.module.js';

var camera, scene, renderer, stats;

var mesh;
var amount = 12;
var count = Math.pow( amount, 3 );
var dummy = new THREE.Object3D();

init();
animate();

function init() {

camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.set( 10, 10, 10 );
camera.lookAt( 0, 0, 0 );

scene = new THREE.Scene();

var material = new THREE.MeshNormalMaterial();
// check overdraw
// var material = new THREE.MeshBasicMaterial( { color: 0xff0000, opacity: 0.1, transparent: true } );


var loader = new THREE.BufferGeometryLoader();
loader.load( 'models/json/suzanne_buffergeometry.json', function ( geometry ) {

geometry.computeVertexNormals();
geometry.scale( 0.5, 0.5, 0.5 );

mesh = new THREE.InstancedMesh( geometry, material, count );
scene.add( mesh );

} );

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should have only one geometry pushed to the GPU.

This works.

var material = new THREE.MeshNormalMaterial();

new THREE.BufferGeometryLoader().load( 'models/json/suzanne_buffergeometry.json', function ( geometry ) {

	geometry.computeVertexNormals();
	geometry.scale( 0.5, 0.5, 0.5 );

	var mesh = new THREE.InstancedMesh( geometry, material, count );
	scene.add( mesh );

} );

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example is still work in progress but... it actually shows that you can easily change geometries at any point, which is something that other PRs didn't make it easy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, I guess you should dispose of the old geometry...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, users copy these examples, so I still would suggest using the conventional pattern I posted above, and then add a GUI that allows the user to switch the geometry and/or material (if you feel it is important to demonstrate that capability). JMO.

//

renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

//

stats = new Stats();
document.body.appendChild( stats.dom );

//

window.addEventListener( 'resize', onWindowResize, false );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

}

//

function animate() {

requestAnimationFrame( animate );

render();

stats.update();

}

function render() {

if ( mesh ) {

var time = Date.now() * 0.001;

mesh.rotation.x = Math.sin( time / 4 );
mesh.rotation.y = Math.sin( time / 2 );

var i = 0;

for ( var x = 0; x < amount; x ++ ) {

for ( var y = 0; y < amount; y ++ ) {

for ( var z = 0; z < amount; z ++ ) {

dummy.position.set( 6 - x, 6 - y, 6 - z );
dummy.rotation.y = ( Math.sin( x / 4 + time ) + Math.sin( y / 4 + time ) + Math.sin( z / 4 + time ) );
dummy.rotation.z = dummy.rotation.y * 2;

dummy.updateMatrix();

mesh.setMatrixAt( i ++, dummy.matrix );

}

}

}

mesh.instanceMatrix.needsUpdate = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I've used THREE.InstancedMesh in a new project and realized it makes sense to add the following line when creating the mesh if instanceMatrix is updated per frame like in this example.

mesh.instanceMatrix.setUsage( THREE.DynamicDrawUsage );


}

renderer.render( scene, camera );

}

</script>

</body>
</html>
1 change: 1 addition & 0 deletions src/Three.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
34 changes: 34 additions & 0 deletions src/objects/InstancedMesh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @author mrdoob / http://mrdoob.com/
*/

import { BufferAttribute } from '../core/BufferAttribute.js';
import { Mesh } from './Mesh.js';

function InstancedMesh( geometry, material, count ) {

Mesh.call( this, geometry, material );

this.instanceMatrix = new BufferAttribute( new Float32Array( count * 16 ), 16 );

}

InstancedMesh.prototype = Object.assign( Object.create( Mesh.prototype ), {

constructor: InstancedMesh,

isInstancedMesh: true,

raycast: function () {},

setMatrixAt: function ( index, matrix ) {

matrix.toArray( this.instanceMatrix.array, index * 16 );

},

updateMorphTargets: function () {}

} );

export { InstancedMesh };
49 changes: 33 additions & 16 deletions src/renderers/WebGLRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function WebGLRenderer( parameters ) {

capabilities = new WebGLCapabilities( _gl, extensions, parameters );

if ( ! capabilities.isWebGL2 ) {
if ( capabilities.isWebGL2 === false ) {

extensions.get( 'WEBGL_depth_texture' );
extensions.get( 'OES_texture_float' );
Expand All @@ -283,7 +283,7 @@ function WebGLRenderer( parameters ) {
textures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info );
attributes = new WebGLAttributes( _gl );
geometries = new WebGLGeometries( _gl, attributes, info );
objects = new WebGLObjects( geometries, info );
objects = new WebGLObjects( _gl, geometries, attributes, info );
morphtargets = new WebGLMorphtargets( _gl );
programCache = new WebGLPrograms( _this, extensions, capabilities );
renderLists = new WebGLRenderLists();
Expand Down Expand Up @@ -765,7 +765,7 @@ function WebGLRenderer( parameters ) {

if ( updateBuffers ) {

setupVertexAttributes( material, program, geometry );
setupVertexAttributes( object, geometry, material, program );

if ( index !== null ) {

Expand Down Expand Up @@ -831,7 +831,6 @@ function WebGLRenderer( parameters ) {

}


} else if ( object.isLine ) {

var lineWidth = material.linewidth;
Expand Down Expand Up @@ -864,13 +863,13 @@ function WebGLRenderer( parameters ) {

}

if ( geometry && geometry.isInstancedBufferGeometry ) {
if ( object.isInstancedMesh ) {

if ( geometry.maxInstancedCount > 0 ) {
renderer.renderInstances( geometry, drawStart, drawCount, object.instanceMatrix.count );

renderer.renderInstances( geometry, drawStart, drawCount );
} else if ( geometry.isInstancedBufferGeometry ) {

}
renderer.renderInstances( geometry, drawStart, drawCount, geometry.maxInstancedCount );

} else {

Expand All @@ -880,16 +879,11 @@ function WebGLRenderer( parameters ) {

};

function setupVertexAttributes( material, program, geometry ) {

if ( geometry && geometry.isInstancedBufferGeometry && ! capabilities.isWebGL2 ) {
function setupVertexAttributes( object, geometry, material, program ) {

if ( extensions.get( 'ANGLE_instanced_arrays' ) === null ) {
if ( capabilities.isWebGL2 === false && ( object.isInstancedMesh || geometry.isInstancedBufferGeometry ) ) {

console.error( 'THREE.WebGLRenderer.setupVertexAttributes: using THREE.InstancedBufferGeometry but hardware does not support extension ANGLE_instanced_arrays.' );
return;

}
if ( extensions.get( 'ANGLE_instanced_arrays' ) === null ) return;

}

Expand Down Expand Up @@ -972,6 +966,29 @@ function WebGLRenderer( parameters ) {

}

} else if ( name === 'instanceMatrix' ) {

var attribute = attributes.get( object.instanceMatrix );

// TODO Attribute may not be available on context restore

if ( attribute === undefined ) continue;

var buffer = attribute.buffer;
var type = attribute.type;

state.enableAttributeAndDivisor( programAttribute + 0, 1 );
state.enableAttributeAndDivisor( programAttribute + 1, 1 );
state.enableAttributeAndDivisor( programAttribute + 2, 1 );
state.enableAttributeAndDivisor( programAttribute + 3, 1 );

_gl.bindBuffer( _gl.ARRAY_BUFFER, buffer );

_gl.vertexAttribPointer( programAttribute + 0, 4, type, false, 64, 0 );
_gl.vertexAttribPointer( programAttribute + 1, 4, type, false, 64, 16 );
_gl.vertexAttribPointer( programAttribute + 2, 4, type, false, 64, 32 );
_gl.vertexAttribPointer( programAttribute + 3, 4, type, false, 64, 48 );

} else if ( materialDefaultAttributeValues !== undefined ) {

var value = materialDefaultAttributeValues[ name ];
Expand Down
10 changes: 9 additions & 1 deletion src/renderers/shaders/ShaderChunk/defaultnormal_vertex.glsl.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export default /* glsl */`
vec3 transformedNormal = normalMatrix * objectNormal;
vec3 transformedNormal = objectNormal;

#ifdef USE_INSTANCING

transformedNormal = mat3( instanceMatrix ) * transformedNormal;

#endif

transformedNormal = normalMatrix * transformedNormal;

#ifdef FLIP_SIDED

Expand Down
10 changes: 9 additions & 1 deletion src/renderers/shaders/ShaderChunk/project_vertex.glsl.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export default /* glsl */`
vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );
vec4 mvPosition = vec4( transformed, 1.0 );

#ifdef USE_INSTANCING

mvPosition = instanceMatrix * mvPosition;

#endif

mvPosition = modelViewMatrix * mvPosition;

gl_Position = projectionMatrix * mvPosition;
`;
10 changes: 9 additions & 1 deletion src/renderers/shaders/ShaderChunk/worldpos_vertex.glsl.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export default /* glsl */`
#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP )

vec4 worldPosition = modelMatrix * vec4( transformed, 1.0 );
vec4 worldPosition = vec4( transformed, 1.0 );

#ifdef USE_INSTANCING

worldPosition = instanceMatrix * worldPosition;

#endif

worldPosition = modelMatrix * worldPosition;

#endif
`;
8 changes: 5 additions & 3 deletions src/renderers/webgl/WebGLBufferRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ function WebGLBufferRenderer( gl, extensions, info, capabilities ) {

}

function renderInstances( geometry, start, count ) {
function renderInstances( geometry, start, count, primcount ) {

if ( primcount === 0 ) return;

var extension, methodName;

Expand All @@ -43,9 +45,9 @@ function WebGLBufferRenderer( gl, extensions, info, capabilities ) {

}

extension[ methodName ]( mode, start, count, geometry.maxInstancedCount );
extension[ methodName ]( mode, start, count, primcount );

info.update( count, mode, geometry.maxInstancedCount );
info.update( count, mode, primcount );

}

Expand Down
8 changes: 5 additions & 3 deletions src/renderers/webgl/WebGLIndexedBufferRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ function WebGLIndexedBufferRenderer( gl, extensions, info, capabilities ) {

}

function renderInstances( geometry, start, count ) {
function renderInstances( geometry, start, count, primcount ) {

if ( primcount === 0 ) return;

var extension, methodName;

Expand All @@ -52,9 +54,9 @@ function WebGLIndexedBufferRenderer( gl, extensions, info, capabilities ) {

}

extension[ methodName ]( mode, count, type, start * bytesPerElement, geometry.maxInstancedCount );
extension[ methodName ]( mode, count, type, start * bytesPerElement, primcount );

info.update( count, mode, geometry.maxInstancedCount );
info.update( count, mode, primcount );

}

Expand Down
Loading