<!--
A little experiment to see how instanced geometries work and how they perform compared to regular geometries.
Yep. There is no HTML.
-->
body { margin: 0; overflow: hidden; background-color: black; }
canvas { width: 100vw; height: 100vh; }
// <<<<<---------------- RECOMMENDED EDITOR WIDTH ---------------->>>>>
// This is an example for the usage of instanced geometries in
// three.js and their incredible performance. It renders a huge
// number of arrow-shapes following their gravitational path
// around an invisible center.
//
// I did my best to write this code in a clean way and added
// lots of comments to make it clear what is happening and why.
// It is meant to be read from top to bottom.
// some constants we'll need later
const NUM_INSTANCES = 6000;
const ARROW_FORWARD = new THREE.Vector3(0, 0, 1);
const UP = new THREE.Vector3(0, 1, 0);
// v3 is used as temporary vector whenever needed. Doing something
// like this can help to significantly reduce the GC-overhead.
const v3 = new THREE.Vector3();
/**
* Initializes the demo.
*
* This function is called way down just before the renderloop
* is started. It returns an update-function that is called inside
* the renderloop for every frame.
*
* This way, we can keep the interesting parts seperated from the
* boring boilerplace of setting up three.js and such.
*
* @return {function(t:Number):void} A function that will be
* called on every frame to update the simulation.
*/
function init(scene) {
// helpers
const numInstances = NUM_INSTANCES;
// instancing basically means that we will be drawing a
// number of instances of the same singular geometry.
// These instances are not controlled by properties
// like mesh.position, mesh.rotation etc. but instead
// using the so called instance-attributes. How many
// instances will be drawn depends on the length of these
// attribute-arrays. So, if the arrays have 10 entries,
// 10 instances will be drawn. Each of these entries can
// have multiple components (e.g. 3 components - x, y and
// z - for the position). This is configured when the
// attributes are created.
// setup instance-attribute buffers
const iOffsets = new Float32Array(numInstances * 3);
const iRotations = new Float32Array(numInstances * 4);
const iColors = new Float32Array(numInstances * 4);
// setup geometry with instance-attributes. The geometry is
// initialized by copying the attributes (position, normal) from
// the arrow-geometry created below in `getArrowGeometry()`.
const geometry = new THREE.InstancedBufferGeometry();
geometry.copy(getArrowGeometry());
// per instance we want to control the position, the rotation
// and the base-color of the arrow. For better readability and
// to avoid conflicts, the instance-attributes are prefixed
// with an 'i'.
geometry.addAttribute('iOffset',
new THREE.InstancedBufferAttribute(iOffsets, 3, 1));
geometry.addAttribute('iRotation',
new THREE.InstancedBufferAttribute(iRotations, 4, 1));
geometry.addAttribute('iColor',
new THREE.InstancedBufferAttribute(iColors, 4, 1));
// we will change position and offset on every frame. I'm not
// sure this has much effect but according to documentation it
// allows the GPU drivers to somehow optimize memory-allocations.
geometry.attributes.iRotation.setDynamic(true);
geometry.attributes.iOffset.setDynamic(true);
// simulation and manipulation of the attribute-buffers happens
// in the Arrow-class defined below. For now just note that
// there is an array of arrows and each of the arrows has an
// instance-index that will be used to compute where its position,
// orientation and color are stored in these attribute-buffers.
const arrows = [];
for (let index = 0; index < numInstances; index++) {
arrows.push(new Arrow(index, {
position: iOffsets,
rotation: iRotations,
color: iColors
}));
}
// create the mesh that now contains all of our instances.
// (the material is defined a bit further down)
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// the real position of instances cannot be determined from the
// mesh-position and bounding-box, so wee need to disable
// the frustrum-culling implemented by three.js
mesh.frustumCulled = false;
// and here is our contribution to the renderloop:
// store the time of the last update
let t0 = performance.now();
return t => {
// limit the maximum timestep per call (otherwise arrows
// will dissappear if you switch away from the tab for a while)
const dt = Math.min((t - t0) / 1000, 0.1);
// update the simulation for all arrows
for (let i=0; i < numInstances; i++) {
arrows[i].update(dt);
}
// mark the instance-attributes as changed so they will
// get uploaded to the GPU
geometry.attributes.iRotation.needsUpdate = true;
geometry.attributes.iOffset.needsUpdate = true;
t0 = t;
};
}
/**
* The Arrow-class implements the logic for simulation and
* buffer-updates for the arrow-instances.
*/
class Arrow {
constructor(index, buffers) {
this.index = index;
this.buffers = buffers;
// the offsets at which data for this arrow is located
// in the attribute-buffers
this.offsets = {
position: index * 3,
rotation: index * 4,
color: index * 4
}
// these properties store the current state of the arrow. those
// will be written to the attribute-buffers after updating.
this.rotation = new THREE.Quaternion();
this.position = new THREE.Vector3();
this.velocity = new THREE.Vector3();
this.color = new THREE.Color();
this.init();
this.update();
}
/**
* Initialize arrow with randomized color, position and velocity.
* Velocity is initially set to be below escape-velocity
* (based on entirely unscientific number tweaking) - otherwise it
* will just look boring.
*/
init() {
// colors won't be modified later, so write them to the
// buffer right away
this.color.setHSL(rnd(0.2, 0.6), 0.2, rnd(0.3, 0.7));
this.color.toArray(this.buffers.color, this.offsets.color);
// for initial positions we're using spherical coordinates, this
// makes it easier to have them evenly distibuted around the
// center.
this.position.setFromSpherical({
radius: rnd(10, 300, 1.6),
phi: Math.PI/2 + rnd(-0.1, 0.1),
theta: rnd(0, 2 * Math.PI)
});
// this is the random-part of the velocity
v3.set(rnd(5), rnd(4), rnd(3));
// compute the initial velocity: perpendicular to the radius,
// somewhat below the escape-velocity, randomized
this.velocity.copy(this.position)
.cross(UP)
.normalize()
.multiplyScalar(Math.PI * Math.PI)
.add(v3);
}
/**
* Update the velocity, position and orientation for a
* given timestep.
*
* NOTE: this is extremely hot code (4000 arrows: ~240k
* calls/second). No allocations and preventable calculations
* here.
*/
update(dt) {
// update velocity from 'gravity' towards origin
v3.copy(this.position)
.multiplyScalar(-Math.PI / this.position.lengthSq());
this.velocity.add(v3);
// update position from velocity
v3.copy(this.velocity).multiplyScalar(dt);
this.position.add(v3);
// update rotation from direction of velocity
v3.copy(this.velocity).normalize();
this.rotation.setFromUnitVectors(ARROW_FORWARD, v3);
// write udpated values into attribute-buffers
this.position.toArray(
this.buffers.position, this.offsets.position);
this.rotation.toArray(
this.buffers.rotation, this.offsets.rotation);
}
}
/**
* Creates the arrow-geometry.
* @return {THREE.BufferGeometry}
*/
function getArrowGeometry() {
const shape = new THREE.Shape([
[-0.8, -1], [-0.03, 1], [-0.01, 1.017], [0.0, 1.0185],
[0.01, 1.017], [0.03, 1], [0.8, -1], [0, -0.5]
].map(p => new THREE.Vector2(...p)));
const arrowGeometry = new THREE.ExtrudeGeometry(shape, {
amount: 0.3,
bevelEnabled: true,
bevelSize: 0.1,
bevelThickness: 0.1,
bevelSegments: 2
});
// orient the geometry into x/z-plane, roughly centered
const matrix = new THREE.Matrix4()
.makeRotationX(Math.PI / 2)
.setPosition(new THREE.Vector3(0, 0.15, 0));
arrowGeometry.applyMatrix(matrix);
// convert to buffer-geometry
return new THREE.BufferGeometry().fromGeometry(arrowGeometry);
}
/**
* The material required to render the instanced geometry. We are
* using a raw shader material so we don't have to deal with possible
* naming-conflics and so on.
*/
const material = new THREE.RawShaderMaterial({
uniforms: {},
vertexShader: `
precision highp float;
// uniforms (all provided by default by three.js)
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;
// default attributes (from arrow-geometry)
attribute vec3 position;
attribute vec3 normal;
// instance attributes
attribute vec3 iOffset;
attribute vec4 iRotation;
attribute vec4 iColor;
// shading-parameters
varying vec3 vLighting;
varying vec4 vColor;
// apply a rotation-quaternion to the given vector
// (source: https://goo.gl/Cq3FU0)
vec3 rotate(const vec3 v, const vec4 q) {
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}
void main() {
// compute lighting (source: https://goo.gl/oS2vIY)
vec3 ambientColor = vec3(1.0) * 0.3;
vec3 directionalColor = vec3(1.0) * 0.7;
vec3 lightDirection = normalize(vec3(-0.5, 1.0, 1.5));
// diffuse-shading
vec3 n = rotate(normalMatrix * normal, iRotation);
vLighting = ambientColor +
(directionalColor * max(dot(n, lightDirection), 0.0));
vColor = iColor;
// instance-transform, mesh-transform and projection
gl_Position = projectionMatrix * modelViewMatrix *
vec4(iOffset + rotate(position, iRotation), 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec3 vLighting;
varying vec4 vColor;
void main() {
gl_FragColor = vColor * vec4(vLighting, 1.0);
gl_FragColor.a = 1.0;
}
`,
side: THREE.DoubleSide,
transparent: false
});
/**
* Random numbers, with range and optional bias.
*/
function rnd(min = 1, max = 0, pow = 1) {
if (arguments.length < 2) {
max = min;
min = 0;
}
const rnd = (pow === 1) ?
Math.random() :
Math.pow(Math.random(), pow);
return (max - min) * rnd + min;
}
// ---- bootstrapping-code
//
// so here's the boring part, the three.js initialization
const width = window.innerWidth;
const height = window.innerHeight;
// create renderer-instance, scene, camera and controls
// .... renderer
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setSize(width, height);
renderer.setClearColor(0x000000);
if (renderer.extensions.get('ANGLE_instanced_arrays') === null) {
console.error('ANGLE_instanced_arrays not supported');
}
// .... scene
const scene = new THREE.Scene();
// .... camera and controls
const camera = new THREE.PerspectiveCamera(
60, width / height, 0.1, 5000);
const controls = new THREE.OrbitControls(camera);
camera.position.set(-80, 50, 20);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// .... run demo-code
// initialize simulation
const update = init(scene, camera);
requestAnimationFrame(function loop(time) {
controls.update();
// update simulation
update(performance.now());
renderer.render(scene, camera);
requestAnimationFrame(loop);
});
// .... bind events
window.addEventListener('resize', ev => {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
});
document.body.appendChild(renderer.domElement);
This Pen doesn't use any external CSS resources.