billboarding vertices in the vertex shader - three.js

Code demonstrating issue
(comment/uncomment out the gl_Position lines in the vertex shader)
var scene;
var book;
var shaderMaterial;
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(0x000000);
document.body.appendChild(renderer.domElement);
var camera = new THREE.PerspectiveCamera(55, 1, 0.1, 40000);
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
window.onresize();
scene = new THREE.Scene();
camera.position.z = 25;
camera.position.y = 15;
scene.add(camera);
var grid = new THREE.GridHelper(100, 10);
scene.add(grid);
var controls = new THREE.OrbitControls(camera);
controls.damping = 0.2;
var lettersPerSide = 16;
function createGlpyhSheet() {
var fontSize = 64;
var c = document.createElement('canvas');
c.width = c.height = fontSize * lettersPerSide;
var ctx = c.getContext('2d');
ctx.font = fontSize + 'px Monospace';
var i = 0;
for (var y = 0; y < lettersPerSide; y++) {
for (var x = 0; x < lettersPerSide; x++, i++) {
var ch = String.fromCharCode(i);
ctx.fillText(ch, x * fontSize, -(8 / 32) * fontSize + (y + 1) * fontSize);
}
}
var tex = new THREE.Texture(c);
tex.flipY = false;
tex.needsUpdate = true;
return tex;
}
function createLabels(textArrays, positions) {
//console.log(textArrays, positions);
var master_geometry = new THREE.Geometry();
for (var k = 0; k < textArrays.length; k++) {
var geo = new THREE.Geometry();
geo.dynamic = true;
var str = textArrays[k];
var vec = positions[k];
//console.log(shaderMaterial);
//console.log('str is', str, 'vec is', vec);
var j = 0,
ln = 0;
for (i = 0; i < str.length; i++) {
//console.log('creating glyph', str[i]);
var code = str.charCodeAt(i);
var cx = code % lettersPerSide;
var cy = Math.floor(code / lettersPerSide);
var oneDotOne = .55;
geo.vertices.push(
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 0.05, 0).add(vec),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 0.05, 0).add(vec),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 1.05, 0).add(vec),
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 1.05, 0).add(vec));
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
var face = new THREE.Face3(i * 4 + 0, i * 4 + 1, i * 4 + 2);
geo.faces.push(face);
face = new THREE.Face3(i * 4 + 0, i * 4 + 2, i * 4 + 3);
geo.faces.push(face);
var ox = (cx + 0.05) / lettersPerSide;
var oy = (cy + 0.05) / lettersPerSide;
var off = 0.9 / lettersPerSide;
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy + off),
new THREE.Vector2(ox + off, oy)]);
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy),
new THREE.Vector2(ox, oy)]);
if (code == 10) {
ln--;
j = 0;
} else {
j++;
}
}
// i can only get this working with merge.
// Building one giant geometry doesn't work for some reason
master_geometry.merge(geo);
}
console.log(shaderMaterial);
shaderMaterial.attributes.labelpos.needsUpdate = true;
book = new THREE.Mesh(
master_geometry,
shaderMaterial);
//book.doubleSided = true;
scene.add(book);
}
var uniforms = {
map: {
type: "t",
value: createGlpyhSheet()
}
};
var attributes = {
labelpos: {
type: 'v3',
value: []
}
};
shaderMaterial = new THREE.ShaderMaterial({
attributes: attributes,
uniforms: uniforms,
vertexShader: document.querySelector('#vertex').textContent,
fragmentShader: document.querySelector('#fragment').textContent
});
shaderMaterial.transparent = true;
shaderMaterial.depthTest = false;
strings = [];
vectors = [];
var sizeOfWorld = 100;
var halfSize = sizeOfWorld * 0.5;
for (var i = 0; i < 500; i++) {
strings.push('test' + i);
var vector = new THREE.Vector3();
vector.x = Math.random() * sizeOfWorld - halfSize;
vector.y = Math.random() * sizeOfWorld - halfSize;
vector.z = Math.random() * sizeOfWorld - halfSize;
vectors.push(vector);
}
console.log('creating labels');
createLabels(strings, vectors);
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate, renderer.domElement);
}
animate();
html {
background-color: #ffffff;
}
* {
margin: 0;
padding: 0;
}
<script src="http://threejs.org/build/three.min.js"></script>
<script src="http://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script id="vertex" type="text/x-glsl-vert">
varying vec2 vUv;
attribute vec3 labelpos;
void main() {
vUv = uv;
// standard gl_Position. Labels stay in the correct place, but do not billboard.
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
// this is the billboarding position as described by:
// http://stackoverflow.com/questions/22053932/three-js-billboard-vertex-shader
//gl_Position = projectionMatrix * (modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0) + vec4(position.x, position.y, 0.0, 0.0));
// this gets a little closer
//gl_Position = projectionMatrix * (modelViewMatrix * vec4(0.0, 0.0, position.z, 1.0) + vec4(position.x, position.y, 0.0, 0.0));
}
</script>
<script id="fragment" type="text/x-glsl-frag">
varying vec2 vUv;
uniform sampler2D map;
void main() {
vec4 diffuse = texture2D(map, vUv);
vec4 letters = mix(diffuse, vec4(1.0, 1.0, 1.0, diffuse.a), 1.0);
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * letters;
}
</script>
I need help billboarding labels in my scene. My final scene will have hundreds of labels which I want to face the camera. I cannot figure out a way of doing this via a single mesh geometry. I've tried a few different gl_Position methods to get the billboarding look:
// standard gl_Position. Labels stay in the correct place, but do not billboard.
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
// this is the billboarding position as described by:
// http://stackoverflow.com/questions/22053932/three-js-billboard-vertex-shader
gl_Position = projectionMatrix * (modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0) + vec4(position.x, position.y, 0.0, 0.0));
// this gets a little closer
gl_Position = projectionMatrix * (modelViewMatrix * vec4(0.0, 0.0, position.z, 1.0) + vec4(position.x, position.y, 0.0, 0.0));
My thinking was to send a shader attribute to each vertex to assist with the billboarding calculation, so that's why I have a label_pos attribute in the vertex shader.
I can get the exact look and feel I want if each label (made up of characters) is added to the scene separately. Unfortunately this results in too many draw calls per render loop, hence the reason for adding them all to a single geometry.
Any assistance on this would be greatly appreciated, thanks.

I think you want
gl_Position = projectionMatrix *
(modelViewMatrix * vec4(labelpos, 1) +
vec4(position.xy, 0, 0));
and you need to not add in the position to the vertices
geo.vertices.push(
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 1.05, 0),
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 1.05, 0));
Otherwise you'd be putting in the position twice.
Because all your labels are in the same mesh then there's only 1 draw call which means you won't get a different location for each label unless you pass it in (which you were in labelpos but you weren't using it)
In which case modelViewMatrix * vec4(0,0,0,1) is the same as just saying modelViewMatrix[3] All you're doing is getting the translation of the model that contains all the labels. That would work if each label was a separate mesh and had its own matrix but since you've put them all in one mesh it won't work.
Your fix was the pass in the location of each label in a separate attribute which you had already included, you just needed to use it.
modelViewMatrix * vec4(labelpos, 1)
gets you the root of the label
vec4(position.x, position.y, 0.0, 0.0)
adds in the corners in view space
var scene;
var book;
var shaderMaterial;
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(0x000000);
document.body.appendChild(renderer.domElement);
var camera = new THREE.PerspectiveCamera(55, 1, 0.1, 40000);
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
window.onresize();
scene = new THREE.Scene();
camera.position.z = 25;
camera.position.y = 15;
scene.add(camera);
var grid = new THREE.GridHelper(100, 10);
scene.add(grid);
var controls = new THREE.OrbitControls(camera);
controls.damping = 0.2;
var lettersPerSide = 16;
function createGlpyhSheet() {
var fontSize = 64;
var c = document.createElement('canvas');
c.width = c.height = fontSize * lettersPerSide;
var ctx = c.getContext('2d');
ctx.font = fontSize + 'px Monospace';
var i = 0;
for (var y = 0; y < lettersPerSide; y++) {
for (var x = 0; x < lettersPerSide; x++, i++) {
var ch = String.fromCharCode(i);
ctx.fillText(ch, x * fontSize, -(8 / 32) * fontSize + (y + 1) * fontSize);
}
}
var tex = new THREE.Texture(c);
tex.flipY = false;
tex.needsUpdate = true;
return tex;
}
function createLabels(textArrays, positions) {
//console.log(textArrays, positions);
var master_geometry = new THREE.Geometry();
for (var k = 0; k < textArrays.length; k++) {
var geo = new THREE.Geometry();
geo.dynamic = true;
var str = textArrays[k];
var vec = positions[k];
//console.log(shaderMaterial);
//console.log('str is', str, 'vec is', vec);
var j = 0,
ln = 0;
for (i = 0; i < str.length; i++) {
//console.log('creating glyph', str[i]);
var code = str.charCodeAt(i);
var cx = code % lettersPerSide;
var cy = Math.floor(code / lettersPerSide);
var oneDotOne = .55;
geo.vertices.push(
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 1.05, 0),
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 1.05, 0));
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
var face = new THREE.Face3(i * 4 + 0, i * 4 + 1, i * 4 + 2);
geo.faces.push(face);
face = new THREE.Face3(i * 4 + 0, i * 4 + 2, i * 4 + 3);
geo.faces.push(face);
var ox = (cx + 0.05) / lettersPerSide;
var oy = (cy + 0.05) / lettersPerSide;
var off = 0.9 / lettersPerSide;
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy + off),
new THREE.Vector2(ox + off, oy)]);
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy),
new THREE.Vector2(ox, oy)]);
if (code == 10) {
ln--;
j = 0;
} else {
j++;
}
}
// i can only get this working with merge.
// Building one giant geometry doesn't work for some reason
master_geometry.merge(geo);
}
console.log(shaderMaterial);
shaderMaterial.attributes.labelpos.needsUpdate = true;
book = new THREE.Mesh(
master_geometry,
shaderMaterial);
//book.doubleSided = true;
scene.add(book);
}
var uniforms = {
map: {
type: "t",
value: createGlpyhSheet()
}
};
var attributes = {
labelpos: {
type: 'v3',
value: []
}
};
shaderMaterial = new THREE.ShaderMaterial({
attributes: attributes,
uniforms: uniforms,
vertexShader: document.querySelector('#vertex').textContent,
fragmentShader: document.querySelector('#fragment').textContent
});
shaderMaterial.transparent = true;
shaderMaterial.depthTest = false;
strings = [];
vectors = [];
var sizeOfWorld = 100;
var halfSize = sizeOfWorld * 0.5;
for (var i = 0; i < 500; i++) {
strings.push('test' + i);
var vector = new THREE.Vector3();
vector.x = Math.random() * sizeOfWorld - halfSize;
vector.y = Math.random() * sizeOfWorld - halfSize;
vector.z = Math.random() * sizeOfWorld - halfSize;
vectors.push(vector);
}
console.log('creating labels');
createLabels(strings, vectors);
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate, renderer.domElement);
}
animate();
html {
background-color: #ffffff;
}
* {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/69/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/4862f5f1111346a957ac3e0cb0858be1568d0e03/examples/js/controls/OrbitControls.js"></script>
<script id="vertex" type="text/x-glsl-vert">
varying vec2 vUv;
attribute vec3 labelpos;
void main() {
vUv = uv;
gl_Position = projectionMatrix *
(modelViewMatrix * vec4(labelpos, 1) +
vec4(position.xy, 0, 0));
}
</script>
<script id="fragment" type="text/x-glsl-frag">
varying vec2 vUv;
uniform sampler2D map;
void main() {
vec4 diffuse = texture2D(map, vUv);
vec4 letters = mix(diffuse, vec4(1.0, 1.0, 1.0, diffuse.a), 1.0);
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * letters;
}
</script>

It is also worth looking at how it is done in Three.js and their SpriteMaterial: sprite_vert.glsl
Here is an annotated snippet:
// optional: pass 2D rotation angle as an uniform
uniform float rotation;
// optional: pass 2D center point as an uniform
uniform vec2 center;
// optional: use this define to scale the model according to distance from the camera
#define USE_SIZEATTENUATION
// [skipped includes]
void main() {
// discard rotation and scale
vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );
// extract model's scale
vec2 scale;
scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );
scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );
// if not defined, keep model the same size regardless of distance from the camera
#ifndef USE_SIZEATTENUATION
bool isPerspective = isPerspectiveMatrix( projectionMatrix );
if ( isPerspective ) scale *= - mvPosition.z;
#endif
// if center is not passed as uniform, create vec2 center = vec2(0.0);
// aligned with the camera [and scaled]
vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;
// if rotation is not passed as uniform, skip the next block
// rotate 2D
vec2 rotatedPosition;
rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;
// billboard
mvPosition.xy += rotatedPosition;
gl_Position = projectionMatrix * mvPosition;
// [skipped includes]
}

Related

mouseposition interaction with THREE.Point

I am learning Three.js and trying to reproduce some cool effects as a training method.
I have a Obj file globe which I want to add some particles interaction to.
my work so far : https://codepen.io/Imsenji/pen/QWOBMWN?editors=0010
What I'm trying to do: https://dala.craftedbygc.com/ ( or similar)
this is my code :
// OBJ model
const obj = new OBJLoader();
obj.load("https://res.cloudinary.com/dltqzyazm/raw/upload/v1648118569/2dgame/globe2_gl4laq.obj", function (object) {
let material = new THREE.PointsMaterial({
color: colour,
size: 0.035,
// map: cross,
});
let mesh = new THREE.Points(object.children[0].geometry, material);
mesh.scale.multiplyScalar(0.025);
mesh.rotation.y = 2 * Math.PI * 0.7;
mesh.position.x = objectsDistance * 0.5;
mesh.position.y = -objectsDistance * 0.1;
scene.add(mesh);
});
//----------------------
* Particles
*/
// Geometry
const particlesCount = 1000;
const positions = new Float32Array(particlesCount * 3);
for (let i = 0; i < particlesCount; i++) {
positions[i * 3 + 0] = (Math.random() - 0.5) * 10;
positions[i * 3 + 1] =
objectsDistance * 0.5 -
Math.random() * objectsDistance * sectionMeshes.length;
positions[i * 3 + 2] = (Math.random() - 0.5) * 10;
}
const particlesGeometry = new THREE.BufferGeometry();
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
// Material
const particlesMaterial = new THREE.PointsMaterial({
color: "#c0a43c",
sizeAttenuation: true,
size: 0.2,
transparent: true,
map: cross,
});
// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);
// // Interaction
var uniforms = {
mouse: {
value: new THREE.Vector3(),
},
radius: {
value: 3.5,
},
};
material.onBeforeCompile = (shader) => {
shader.uniforms.mouse = uniforms.mouse;
shader.uniforms.radius = uniforms.radius;
// console.log(shader.vertexShader);
shader.vertexShader =
`
uniform vec3 mouse;
uniform float radius;
` + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(
`#include <begin_vertex>`,
`#include <begin_vertex>
vec3 dir = transformed - mouse;
float dist = length(dir);
if (dist < radius){
float ratio = 1. - dist / radius;
vec3 shift = dir * 2. * (ratio * ratio);
transformed += shift;
}
`
);
};
var plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
window.addEventListener("mousemove", (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(plane, uniforms.mouse.value);
});
it seems like raycaster.ray.intersectPlane(plane, uniforms.mouse.value); not working. that's my question.
any help/advice would be appreciated.

can't get a Three.js InstancedBufferGeometry to appear in a scene

I can't seem to get an InstancedBufferGeometry to appear in a scene anymore. I've tried the following technique:
let bg = BoxBufferGeometry(3,3,3),
ig : InstancedBufferGeometry = new InstancedBufferGeometry(),
mesh : Mesh;
ig.index = bg.index;
ig.attributes.position = bg.attributes.position;
ig.attributes.uv = bg.attributes.uv;
//ig.copy( bg ); <- also tried this
ig.instanceCount = Infinity; <- otherwise this populates as undefined
var offsets = [];
var orientations = [];
var vector = new Vector4();
var x, y, z, w;
for ( var i = 0; i < 1000; i ++ ) {
// offsets
x = Math.random() * 100 - 50;
y = Math.random() * 100 - 50;
z = Math.random() * 100 - 50;
vector.set( x, y, z, 0 ).normalize();
vector.multiplyScalar( 5 );
offsets.push( x + vector.x, y + vector.y, z + vector.z );
// orientations
x = Math.random() * 2 - 1;
y = Math.random() * 2 - 1;
z = Math.random() * 2 - 1;
w = Math.random() * 2 - 1;
vector.set( x, y, z, w ).normalize();
orientations.push( vector.x, vector.y, vector.z, vector.w );
}
var offsetAttribute = new InstancedBufferAttribute( new Float32Array( offsets ), 3 );
var orientationAttribute = new InstancedBufferAttribute( new Float32Array( orientations ), 4 ).setUsage( DynamicDrawUsage );
ig.setAttribute( 'offset', offsetAttribute );
ig.setAttribute( 'orientation', orientationAttribute );
let m = new MeshPhongMaterial( { color: 0xdddddd, specular: 0x009900, shininess: 30, flatShading: true } );
mesh = new Mesh( ig, m );
scene.add(mesh);
I don't get any errors and the mesh does appear in the scene when I inspect. I've tried all combinations of attributes in the IBG.
Will this approach no longer work? (It worked in r97... I know, ages ago. )
let m = new MeshPhongMaterial( { color: 0xdddddd, specular: 0x009900, shininess: 30, flatShading: true } );
When using InstancedBufferGeometry, you can't create a mesh with built-in materials like MeshPhongMaterial. Instead you have to create a custom shader that utilizes the instanced attributes. Example:
var camera, scene, renderer;
init();
animate();
function init() {
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 100;
scene = new THREE.Scene();
var geometry = new THREE.BoxBufferGeometry();
var ig = new THREE.InstancedBufferGeometry();
ig.index = geometry.index;
ig.attributes.position = geometry.attributes.position;
var offsets = [];
var orientations = [];
var vector = new THREE.Vector4();
var x, y, z, w;
for (var i = 0; i < 1000; i++) {
// offsets
x = Math.random() * 100 - 50;
y = Math.random() * 100 - 50;
z = Math.random() * 100 - 50;
vector.set(x, y, z, 0).normalize();
offsets.push(x + vector.x, y + vector.y, z + vector.z);
// orientations
x = Math.random() * 2 - 1;
y = Math.random() * 2 - 1;
z = Math.random() * 2 - 1;
w = Math.random() * 2 - 1;
vector.set(x, y, z, w).normalize();
orientations.push(vector.x, vector.y, vector.z, vector.w);
}
var offsetAttribute = new THREE.InstancedBufferAttribute(new Float32Array(offsets), 3);
var orientationAttribute = new THREE.InstancedBufferAttribute(new Float32Array(orientations), 4);
ig.setAttribute('offset', offsetAttribute);
ig.setAttribute('orientation', orientationAttribute);
var material = new THREE.RawShaderMaterial({
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent,
side: THREE.DoubleSide
});
mesh = new THREE.Mesh(ig, material);
scene.add(mesh);
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
body {
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.124/build/three.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
attribute vec3 offset;
attribute vec4 orientation;
void main(){
vec3 pos = offset + position;
vec3 vcV = cross( orientation.xyz, pos );
pos = vcV * ( 2.0 * orientation.w ) + ( cross( orientation.xyz, vcV ) * 2.0 + pos );
gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
precision highp float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
</script>
Consider to use InstancedMesh for a more straightforward usage of instanced rendering.

How to Create a Multi-Colored Noise Shader

I need a shader that takes 3 input colors and generates noise as seen below.
It is easy to achieve in Blender with the help of the "Noise Texture" and the "Color Ramp" nodes.
I've found this gist, which might solve my problem. But I wasn't able to configure the colors. And also the noise looks a lot "sharper" than the result in Blender.
Will I need to write my own shader for this or is there a simpler way to achieve this effect with ThreeJS?
Basically, you need just two things:
Noise function in shader
A gradiental texture.
I chose FBM for noise and used .onBeforeCompile() to change a built-in material (Standard):
body{
overflow: hidden;
margin: 0;
}
<canvas id="cnvsGradient" width="300" height="50" style="position: absolute; margin: 10px; border: 1px solid aqua"/>
<script>
// https://github.com/yiwenl/glsl-fbm/blob/master/3d.glsl
const fbm = `
#define NUM_OCTAVES 5
float mod289(float x){return x - floor(x * (1.0 / 289.0)) * 289.0;}
vec4 mod289(vec4 x){return x - floor(x * (1.0 / 289.0)) * 289.0;}
vec4 perm(vec4 x){return mod289(((x * 34.0) + 1.0) * x);}
float noise(vec3 p){
vec3 a = floor(p);
vec3 d = p - a;
d = d * d * (3.0 - 2.0 * d);
vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0);
vec4 k1 = perm(b.xyxy);
vec4 k2 = perm(k1.xyxy + b.zzww);
vec4 c = k2 + a.zzzz;
vec4 k3 = perm(c);
vec4 k4 = perm(c + 1.0);
vec4 o1 = fract(k3 * (1.0 / 41.0));
vec4 o2 = fract(k4 * (1.0 / 41.0));
vec4 o3 = o2 * d.z + o1 * (1.0 - d.z);
vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x);
return o4.y * d.y + o4.x * (1.0 - d.y);
}
float fbm(vec3 x) {
float v = 0.0;
float a = 0.5;
vec3 shift = vec3(100);
for (int i = 0; i < NUM_OCTAVES; ++i) {
v += a * noise(x);
x = x * 2.0 + shift;
a *= 0.5;
}
return v;
}
`;
</script>
<script type="module">
console.clear();
import * as THREE from "https://cdn.skypack.dev/three#0.136.0";
import {OrbitControls} from "https://cdn.skypack.dev/three#0.136.0/examples/jsm/controls/OrbitControls.js";
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 100);
camera.position.set(0, 0, 10);
let renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x444444);
document.body.appendChild(renderer.domElement);
let controls = new OrbitControls(camera, renderer.domElement);
let light = new THREE.DirectionalLight(0xffffff);
light.position.setScalar(10);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
let g = new THREE.CylinderBufferGeometry(2, 1, 5, 6, 64);
let pos = g.attributes.position;
let v = new THREE.Vector3();
let axis = new THREE.Vector3(0, 1, 0);
for(let i = 0; i < pos.count; i++){
v.fromBufferAttribute(pos, i);
let ratio = (v.y - (-2.5)) / 5;
v.applyAxisAngle(axis, THREE.MathUtils.degToRad(60) * ratio);
pos.setXYZ(i, v.x, v.y, v.z);
}
g.computeVertexNormals();
let uniforms = {
tex: {
value: setGradient()
}
}
let m = new THREE.MeshStandardMaterial({
metalness: 0.25,
roughness: 0.75,
onBeforeCompile: shader => {
shader.uniforms.tex = uniforms.tex;
shader.vertexShader = `
varying vec3 vPos;
${shader.vertexShader}
`.replace(
`#include <begin_vertex>`,
`#include <begin_vertex>
//vPos = (modelMatrix * vec4(position, 1.0)).xyz;
vPos = vec3(position);
`
);
//console.log(shader.vertexShader);
shader.fragmentShader = `
uniform sampler2D tex;
varying vec3 vPos;
${fbm}
${shader.fragmentShader}
`.replace(
`vec4 diffuseColor = vec4( diffuse, opacity );`,
`
float d = fbm(vPos * 0.5);
for(int i = 0; i < 4; i++){
d = fbm(vPos * (float(i) + 1.) * d);
}
vec3 col = texture(tex, vec2(d, 0.5)).rgb;
vec4 diffuseColor = vec4( col, opacity );`
);
//console.log(shader.fragmentShader);
}
});
let o = new THREE.Mesh(g, m);
scene.add(o);
window.addEventListener( 'resize', onWindowResize, false );
renderer.setAnimationLoop(() => {
o.rotation.y += 0.01;
renderer.render(scene, camera);
});
function setGradient(){
let canvas = document.getElementById('cnvsGradient');
let ctx = canvas.getContext('2d');
let gradient = ctx.createLinearGradient(0,0, 300,0);
gradient.addColorStop(0.15, 'yellow');
gradient.addColorStop(.5, 'red');
gradient.addColorStop(0.85, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return new THREE.CanvasTexture(canvas);
}
function onWindowResize() {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( innerWidth, innerHeight );
}
</script>

shaderMaterial not work width Points

function createParticles(imgData) {
const geometry = new THREE.BufferGeometry();
var c = 0, x = 0, y = 0, positions = [], colors = [];
var data = imgData.data;
x = -imgData.width * 0.5;
y = imgData.height * 0.5;
for (var i = 0; i < imgData.width; i++) {
for (var j = 0; j < imgData.height; j++) {
positions.push(i - imgData.width * 0.5, j - imgData.height * 0.5, 0);
}
}
geometry.addAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
var material = new THREE.ShaderMaterial({ fragmentShader: document.getElementById('f-shader').textContent });
return new THREE.Points(geometry, material);
}
and this is my shader:
<script type="shader" id="f-shader">
void main(){
gl_FragColor = vec4(1.0, 0.58, 0.86, 1.0);
}
</script>
i add this shadermaterial ,but there is nothing ,and the points will show when no material
You will also need to specify a vertex shader, and the vertex shader will have to have a gl_PointSize call in it. Something like:
void main() {
gl_PointSize = 10.0;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}

Buffered Geometry billboarding vertices in the vertex shader

I came across this truly excellent example of how to implement billboarding via a vertex shader to offload the hard work of drawing and rotating a large number of labels to always face the camera.
var scene;
var book;
var shaderMaterial;
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(0x000000);
document.body.appendChild(renderer.domElement);
var camera = new THREE.PerspectiveCamera(55, 1, 0.1, 40000);
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
window.onresize();
scene = new THREE.Scene();
camera.position.z = 25;
camera.position.y = 15;
scene.add(camera);
var grid = new THREE.GridHelper(100, 10);
scene.add(grid);
var controls = new THREE.OrbitControls(camera);
controls.damping = 0.2;
var lettersPerSide = 16;
function createGlpyhSheet() {
var fontSize = 64;
var c = document.createElement('canvas');
c.width = c.height = fontSize * lettersPerSide;
var ctx = c.getContext('2d');
ctx.font = fontSize + 'px Monospace';
var i = 0;
for (var y = 0; y < lettersPerSide; y++) {
for (var x = 0; x < lettersPerSide; x++, i++) {
var ch = String.fromCharCode(i);
ctx.fillText(ch, x * fontSize, -(8 / 32) * fontSize + (y + 1) * fontSize);
}
}
var tex = new THREE.Texture(c);
tex.flipY = false;
tex.needsUpdate = true;
return tex;
}
function createLabels(textArrays, positions) {
//console.log(textArrays, positions);
var master_geometry = new THREE.Geometry();
for (var k = 0; k < textArrays.length; k++) {
var geo = new THREE.Geometry();
geo.dynamic = true;
var str = textArrays[k];
var vec = positions[k];
//console.log(shaderMaterial);
//console.log('str is', str, 'vec is', vec);
var j = 0,
ln = 0;
for (i = 0; i < str.length; i++) {
//console.log('creating glyph', str[i]);
var code = str.charCodeAt(i);
var cx = code % lettersPerSide;
var cy = Math.floor(code / lettersPerSide);
var oneDotOne = .55;
geo.vertices.push(
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 1.05, 0),
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 1.05, 0));
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
shaderMaterial.attributes.labelpos.value.push(vec);
var face = new THREE.Face3(i * 4 + 0, i * 4 + 1, i * 4 + 2);
geo.faces.push(face);
face = new THREE.Face3(i * 4 + 0, i * 4 + 2, i * 4 + 3);
geo.faces.push(face);
var ox = (cx + 0.05) / lettersPerSide;
var oy = (cy + 0.05) / lettersPerSide;
var off = 0.9 / lettersPerSide;
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy + off),
new THREE.Vector2(ox + off, oy)]);
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy),
new THREE.Vector2(ox, oy)]);
if (code == 10) {
ln--;
j = 0;
} else {
j++;
}
}
// i can only get this working with merge.
// Building one giant geometry doesn't work for some reason
master_geometry.merge(geo);
}
console.log(shaderMaterial);
shaderMaterial.attributes.labelpos.needsUpdate = true;
book = new THREE.Mesh(
master_geometry,
shaderMaterial);
//book.doubleSided = true;
scene.add(book);
}
var uniforms = {
map: {
type: "t",
value: createGlpyhSheet()
}
};
var attributes = {
labelpos: {
type: 'v3',
value: []
}
};
shaderMaterial = new THREE.ShaderMaterial({
attributes: attributes,
uniforms: uniforms,
vertexShader: document.querySelector('#vertex').textContent,
fragmentShader: document.querySelector('#fragment').textContent
});
shaderMaterial.transparent = true;
shaderMaterial.depthTest = false;
strings = [];
vectors = [];
var sizeOfWorld = 100;
var halfSize = sizeOfWorld * 0.5;
for (var i = 0; i < 500; i++) {
strings.push('test' + i);
var vector = new THREE.Vector3();
vector.x = Math.random() * sizeOfWorld - halfSize;
vector.y = Math.random() * sizeOfWorld - halfSize;
vector.z = Math.random() * sizeOfWorld - halfSize;
vectors.push(vector);
}
console.log('creating labels');
createLabels(strings, vectors);
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate, renderer.domElement);
}
animate();
html {
background-color: #ffffff;
}
* {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/69/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/4862f5f1111346a957ac3e0cb0858be1568d0e03/examples/js/controls/OrbitControls.js"></script>
<script id="vertex" type="text/x-glsl-vert">
varying vec2 vUv;
attribute vec3 labelpos;
void main() {
vUv = uv;
gl_Position = projectionMatrix *
(modelViewMatrix * vec4(labelpos, 1) +
vec4(position.xy, 0, 0));
}
</script>
<script id="fragment" type="text/x-glsl-frag">
varying vec2 vUv;
uniform sampler2D map;
void main() {
vec4 diffuse = texture2D(map, vUv);
vec4 letters = mix(diffuse, vec4(1.0, 1.0, 1.0, diffuse.a), 1.0);
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * letters;
}
</script>
The code was created before THREE js transitioned away from allowing attributes in uniforms, and enforcing we now use buffered Geometry instead. After some digging I found you can easily create a buffered Geometry from a standard geometry using:
buffGeometry = new THREE.BufferGeometry().fromGeometry( <my old Geometry object> );
How cool is that! - works a treat, however I cannot work out how or where to pass the long list of attribute vec3's to the shader to tell it where my mid point for each label should be, to achieve the same effect as the older example given.
Has anyone got any ideas on how to solve this? The example posted is exactly what I am after, but I really don't want to be stuck using an old version of THREE for the rest of time...
Many thanks for any suggestions :)
FR
So after much experimentation I figured it out myself - go me.
You convert the old Geometry object to a THREE.BufferGeometry() using the aforementioned fromGeometry() function, create an Float32Array array of the location of each labels x,y,z coordinates for each and every vertices and pass that array to the BufferGeometry via the addAttribute function, the shader knows both where to draw the labels and where to pivot when rotating the camera, re-creating the billboard effect using the latest version of THREE.js. 8) See working attached code example, hope someone else finds this useful! :)
var scene;
var book;
var shaderMaterial;
var stats;
var container;
container = document.createElement('div');
document.body.appendChild(container);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(0x000000);
document.body.appendChild(renderer.domElement);
var camera = new THREE.PerspectiveCamera(55, 1, 0.1, 40000);
window.onresize = function() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
window.onresize();
scene = new THREE.Scene();
camera.position.z = 25;
camera.position.y = 15;
scene.add(camera);
var labelPosArray = [];
var grid = new THREE.GridHelper(100, 10);
scene.add(grid);
stats = new Stats();
container.appendChild(stats.dom);
container.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.damping = 0.2;
var lettersPerSide = 16;
function createGlpyhSheet() {
var fontSize = 64;
var c = document.createElement('canvas');
c.width = c.height = fontSize * lettersPerSide;
var ctx = c.getContext('2d');
ctx.font = fontSize + 'px Monospace';
var i = 0;
for (var y = 0; y < lettersPerSide; y++) {
for (var x = 0; x < lettersPerSide; x++, i++) {
var ch = String.fromCharCode(i);
ctx.fillText(ch, x * fontSize, -(8 / 32) * fontSize + (y + 1) * fontSize);
}
}
var tex = new THREE.Texture(c);
tex.flipY = false;
tex.needsUpdate = true;
return tex;
}
function createLabels(textArrays, positions) {
var master_geometry = new THREE.Geometry();
for (var k = 0; k < textArrays.length; k++) {
var geo = new THREE.Geometry();
geo.dynamic = true;
var str = textArrays[k];
var vec = positions[k];
var j = 0,
ln = 0;
for (i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
var cx = code % lettersPerSide;
var cy = Math.floor(code / lettersPerSide);
var oneDotOne = .55;
geo.vertices.push(
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 0.05, 0),
new THREE.Vector3(j * oneDotOne + 1.05, ln * oneDotOne + 1.05, 0),
new THREE.Vector3(j * oneDotOne + 0.05, ln * oneDotOne + 1.05, 0));
labelPosArray.push(vec);
labelPosArray.push(vec);
labelPosArray.push(vec);
labelPosArray.push(vec);
labelPosArray.push(vec);
labelPosArray.push(vec);
var face = new THREE.Face3(i * 4 + 0, i * 4 + 1, i * 4 + 2);
geo.faces.push(face);
face = new THREE.Face3(i * 4 + 0, i * 4 + 2, i * 4 + 3);
geo.faces.push(face);
var ox = (cx + 0.05) / lettersPerSide;
var oy = (cy + 0.05) / lettersPerSide;
var off = 0.9 / lettersPerSide;
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy + off),
new THREE.Vector2(ox + off, oy)
]);
geo.faceVertexUvs[0].push([
new THREE.Vector2(ox, oy + off),
new THREE.Vector2(ox + off, oy),
new THREE.Vector2(ox, oy)
]);
if (code == 10) {
ln--;
j = 0;
} else {
j++;
}
}
master_geometry.merge(geo);
}
var lps = new Float32Array(labelPosArray.length * 3);
var cnt = 0;
for (i = 0; i < labelPosArray.length; i++) {
lps[cnt++] = labelPosArray[i].x;
lps[cnt++] = labelPosArray[i].y;
lps[cnt++] = labelPosArray[i].z;
} // for
buffGeometry = new THREE.BufferGeometry().fromGeometry(master_geometry);
buffGeometry.addAttribute('labelpos', new THREE.BufferAttribute(lps, 3));
book = new THREE.Mesh(
buffGeometry,
shaderMaterial);
scene.add(book);
}
var uniforms = {
map: {
type: "t",
value: createGlpyhSheet()
}
};
shaderMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: document.querySelector('#vertex').textContent,
fragmentShader: document.querySelector('#fragment').textContent
});
shaderMaterial.transparent = true;
shaderMaterial.depthTest = false;
strings = [];
vectors = [];
var sizeOfWorld = 100;
var halfSize = sizeOfWorld * 0.5;
for (var i = 0; i < 500; i++) {
strings.push('label ' + i);
var vector = new THREE.Vector3();
vector.x = Math.random() * sizeOfWorld - halfSize;
vector.y = Math.random() * sizeOfWorld - halfSize;
vector.z = Math.random() * sizeOfWorld - halfSize;
vectors.push(vector);
}
//console.log('creating labels');
createLabels(strings, vectors);
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate, renderer.domElement);
stats.update();
}
animate();
html {
background-color: #ffffff;
}
* {
margin: 0;
padding: 0;
}
<script src="https://raw.githack.com/mrdoob/three.js/r124/build/three.js"></script>
<script src="https://raw.githack.com/mrdoob/three.js/r124/examples/js/controls/OrbitControls.js"></script>
<script src="https://raw.githack.com/mrdoob/three.js/r124/examples/js/libs/stats.min.js"></script>
<script id="vertex" type="text/x-glsl-vert">
varying vec2 vUv; attribute vec3 labelpos; void main() { vUv = uv; gl_Position = projectionMatrix * (modelViewMatrix * vec4(labelpos, 1) + vec4(position.xy, 0, 0)); }
</script>
<script id="fragment" type="text/x-glsl-frag">
varying vec2 vUv; uniform sampler2D map; void main() { vec4 diffuse = texture2D(map, vUv); vec4 letters = mix(diffuse, vec4(1.0, 1.0, 1.0, diffuse.a), 1.0); gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) * letters; }
</script>

Resources