Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save joseph45666/bc1e16bd28c478da3415107c37b080e1 to your computer and use it in GitHub Desktop.
Save joseph45666/bc1e16bd28c478da3415107c37b080e1 to your computer and use it in GitHub Desktop.
Gravity (three.js / instancing / glsl)
<!--
A little experiment to see how instanced geometries work and how they perform compared to regular geometries.
Yep. There is no HTML.
-->
// <<<<<---------------- RECOMMENDED EDITOR WIDTH ---------------->>>>>
// some constants
const ARROW_FORWARD = new THREE.Vector3(0, 0, 1);
const UP = new THREE.Vector3(0, 1, 0);
// v3 is used as temporary vector where needed
const v3 = new THREE.Vector3();
/**
* Initializes the demo.
*/
function init(scene) {
// helpers
const numInstances = 4000;
// 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
const geometry = new THREE.InstancedBufferGeometry();
geometry.copy(getArrowGeometry());
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));
geometry.attributes.iRotation.setDynamic(true);
geometry.attributes.iOffset.setDynamic(true);
// attach entities
const arrows = [];
for (let i = 0; i < numInstances; i++) {
arrows.push(new Arrow(i, {
position: iOffsets,
rotation: iRotations,
color: iColors
}));
}
const mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = false;
scene.add(mesh);
// return the renderloop-function
let t0 = performance.now();
return t => {
// limit the maximum timestep per renderframe (otherwise arrows
// will dissappear if you switch away from the tab for a while)
const dt = Math.min((t - t0) / 1000, 0.1);
for (let i=0; i < numInstances; i++) {
arrows[i].update(dt);
}
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;
this.offsets = {
position: index * 3,
rotation: index * 4,
color: index * 4
}
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 computed to always™ be below escape-velocity
* (based on entirely unscientific number tweaking).
*/
init() {
this.color.setHSL(rnd(0.5, 0.7), 0.3, rnd(0.4, 0.6));
this.color.toArray(this.buffers.color, this.offsets.color);
this.position.setFromSpherical({
radius: rnd(10, 300, 1.6),
phi: Math.PI/2 + rnd(-0.1, 0.1),
theta: rnd(2 * Math.PI)
});
v3.set(rnd(5), rnd(4), rnd(3));
this.velocity.copy(this.position)
.cross(UP)
// prevent too many arrows from reaching escape-velocity
.multiplyScalar(9/this.position.length())
.add(v3);
}
/**
* Update the velocity, position and orientation for a
* given timestep.
*
* NOTE: extremely hot code (4000 arrows: ~240k calls/second),
* avoid allocations and preventable calculations.
*/
update(dt) {
// update velocity from 'gravity' towards origin
v3.copy(this.position)
.multiplyScalar(-3 / 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 to 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 geom = new THREE.ExtrudeGeometry(shape, {
amount: 0.3, bevelSize: 0.1, bevelThickness: 0.1, bevelSegments: 5
});
const mat = new THREE.Matrix4()
.makeRotationX(Math.PI / 2)
.setPosition(new THREE.Vector3(0, 0.15, 0));
geom.applyMatrix(mat);
return new THREE.BufferGeometry().fromGeometry(geom);
}
/**
* The material required to render the instanced geometry.
*/
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
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;
vec3 rotate(const vec3 v, const vec4 q) {
// apply rotation (https://goo.gl/Cq3FU0)
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}
void main() {
// compute lighting (https://goo.gl/oS2vIY)
vec3 ambientColor = vec3(1.0) * 0.4;
vec3 directionalColor = vec3(1.0) * 0.8;
vec3 lightDirection = vec3(0.0, 0.0, 1.0);
// 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);
}
`,
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
const width = window.innerWidth;
const height = window.innerHeight;
// .... renderer
const renderer = new THREE.WebGLRenderer({
alpha: true, antialias: true
});
renderer.setSize(width, height);
// .... 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
const update = init(scene, camera);
requestAnimationFrame(function loop(time) {
controls.update();
if (update) { 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);
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/build/three.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.rawgit.com/wearekuva/oui/master/dist/oui.js"></script>
body { margin: 0; overflow: hidden; background-color: black; }
canvas { width: 100vw; height: 100vh; }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment