Port shader with multiple buffers from shadertoy - three.js

Im trying to understand how to implement multiple buffers in three.js by porting a shader from shadertoy with help of this thread.
https://discourse.threejs.org/t/help-porting-shadertoy-to-threejs/
I tried to rewrite it for js but it doesnt compile.
Here is the code:
https://codepen.io/haangglide/pen/BaKXmLX
It is based on this one:
https://www.shadertoy.com/view/4sG3WV
My understanding of using buffers is:
create a bufferscene
bufferAscene = new THREE.Scene();
create a texture
textureA = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
minFilter: THREE.LinearFilter,
magFilter: THREE.NearestFilter
});
create a shadermaterial where you define the uniforms for passing to the shader
bufferA = new THREE.ShaderMaterial({
uniforms: {
iFrame: { value: 0 },
iResolution: { value: resolution },
iMouse: { value: mousePosition },
iChannel0: { value: textureA.texture },
iChannel1: { value: textureB.texture }
},
vertexShader: VERTEX_SHADER,
fragmentShader: BUFFER_A_FRAG,
});
create a PlaneBufferGeometry and create a mesh from the geometry and buffermaterial
new THREE.Mesh(planeA, bufferA)
add it to the Scene
bufferAscene.add(new THREE.Mesh(planeA, bufferA));
In the render:
update the uniforms
bufferA.uniforms.iChannel0.value = textureA
I dont really understand the swap though.
If anyone can help me to get the application to compile it would be very much apreciated!

Here is a live example that ported the original TS code to JavaScript.
// Port from Shadertoy to THREE.js: https://www.shadertoy.com/view/4sG3WV
const VERTEX_SHADER = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`;
const BUFFER_A_FRAG = `
uniform vec4 iMouse;
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform vec2 iResolution;
uniform float iFrame;
varying vec2 vUv;
#define mousedata(a,b) texture2D( iChannel1, (0.5+vec2(a,b)) / iResolution.xy, -0.0 )
#define backbuffer(uv) texture2D( iChannel0, uv ).xy
float lineDist(vec2 p, vec2 start, vec2 end, float width) {
vec2 dir = start - end;
float lngth = length(dir);
dir /= lngth;
vec2 proj = max(0.0, min(lngth, dot((start - p), dir))) * dir;
return length( (start - p) - proj ) - (width / 2.0);
}
void main() {
vec2 uv = vUv;
vec2 col = uv;
if (iFrame > 2.) {
col = texture2D(iChannel0,uv).xy;
vec2 mouse = iMouse.xy/iResolution.xy;
vec2 p_mouse = mousedata(2.,0.).xy;
if (mousedata(4.,0.).x > 0.) {
col = backbuffer(uv+((p_mouse-mouse)*clamp(1.-(lineDist(uv,mouse,p_mouse,0.)*20.),0.,1.)*.7));
}
}
gl_FragColor = vec4(col,0.0,1.0);
}
`;
const BUFFER_B_FRAG = `
uniform vec4 iMouse;
uniform sampler2D iChannel0;
uniform vec3 iResolution;
varying vec2 vUv;
bool pixelAt(vec2 coord, float a, float b) {
return (floor(coord.x) == a && floor(coord.y) == b);
}
vec4 backbuffer(float a,float b) {
return texture2D( iChannel0, (0.5+vec2(a,b)) / iResolution.xy, -100.0 );
}
void main( ) {
vec2 uv = vUv;// / iResolution.xy;
vec4 color = texture2D(iChannel0,uv);
if (pixelAt(gl_FragCoord.xy,0.,0.)) { //Surface position
gl_FragColor = vec4(backbuffer(0.,0.).rg+(backbuffer(4.,0.).r*(backbuffer(2.,0.).rg-backbuffer(1.,0.).rg)),0.,1.);
} else if (pixelAt(gl_FragCoord.xy,1.,0.)) { //New mouse position
gl_FragColor = vec4(iMouse.xy/iResolution.xy,0.,1.);
} else if (pixelAt(gl_FragCoord.xy,2.,0.)) { //Old mouse position
gl_FragColor = vec4(backbuffer(1.,0.).rg,0.,1.);
} else if (pixelAt(gl_FragCoord.xy,3.,0.)) { //New mouse holded
gl_FragColor = vec4(clamp(iMouse.z,0.,1.),0.,0.,1.);
} else if (pixelAt(gl_FragCoord.xy,4.,0.)) { //Old mouse holded
gl_FragColor = vec4(backbuffer(3.,0.).r,0.,0.,1.);
} else {
gl_FragColor = vec4(0.,0.,0.,1.);
}
}
`;
const BUFFER_FINAL_FRAG = `
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
varying vec2 vUv;
void main() {
vec2 uv = vUv;
vec2 a = texture2D(iChannel1,uv).xy;
gl_FragColor = vec4(texture2D(iChannel0,a).rgb,1.0);
}
`;
class App {
constructor() {
this.width = 1024;
this.height = 512;
this.renderer = new THREE.WebGLRenderer();
this.loader = new THREE.TextureLoader();
this.mousePosition = new THREE.Vector4();
this.orthoCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
this.counter = 0;
this.renderer.setSize(this.width, this.height);
document.body.appendChild(this.renderer.domElement);
this.renderer.domElement.addEventListener('mousedown', () => {
this.mousePosition.setZ(1);
this.counter = 0;
});
this.renderer.domElement.addEventListener('mouseup', () => {
this.mousePosition.setZ(0);
});
this.renderer.domElement.addEventListener('mousemove', event => {
this.mousePosition.setX(event.clientX);
this.mousePosition.setY(this.height - event.clientY);
});
this.targetA = new BufferManager(this.renderer, {
width: this.width,
height: this.height
});
this.targetB = new BufferManager(this.renderer, {
width: this.width,
height: this.height
});
this.targetC = new BufferManager(this.renderer, {
width: this.width,
height: this.height
});
}
start() {
const resolution = new THREE.Vector3(this.width, this.height, window.devicePixelRatio);
const channel0 = this.loader.load('https://res.cloudinary.com/di4jisedp/image/upload/v1523722553/wallpaper.jpg');
this.loader.setCrossOrigin('');
this.bufferA = new BufferShader(BUFFER_A_FRAG, {
iFrame: {
value: 0
},
iResolution: {
value: resolution
},
iMouse: {
value: this.mousePosition
},
iChannel0: {
value: null
},
iChannel1: {
value: null
}
});
this.bufferB = new BufferShader(BUFFER_B_FRAG, {
iFrame: {
value: 0
},
iResolution: {
value: resolution
},
iMouse: {
value: this.mousePosition
},
iChannel0: {
value: null
}
});
this.bufferImage = new BufferShader(BUFFER_FINAL_FRAG, {
iResolution: {
value: resolution
},
iMouse: {
value: this.mousePosition
},
iChannel0: {
value: channel0
},
iChannel1: {
value: null
}
});
this.animate();
}
animate() {
requestAnimationFrame(() => {
this.bufferA.uniforms['iFrame'].value = this.counter++;
this.bufferA.uniforms['iChannel0'].value = this.targetA.readBuffer.texture;
this.bufferA.uniforms['iChannel1'].value = this.targetB.readBuffer.texture;
this.targetA.render(this.bufferA.scene, this.orthoCamera);
this.bufferB.uniforms['iChannel0'].value = this.targetB.readBuffer.texture;
this.targetB.render(this.bufferB.scene, this.orthoCamera);
this.bufferImage.uniforms['iChannel1'].value = this.targetA.readBuffer.texture;
this.targetC.render(this.bufferImage.scene, this.orthoCamera, true);
this.animate();
});
}
}
class BufferShader {
constructor(fragmentShader, uniforms = {}) {
this.uniforms = uniforms;
this.material = new THREE.ShaderMaterial({
fragmentShader: fragmentShader,
vertexShader: VERTEX_SHADER,
uniforms: uniforms
});
this.scene = new THREE.Scene();
this.scene.add(
new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), this.material)
);
}
}
class BufferManager {
constructor(renderer, size) {
this.renderer = renderer;
this.readBuffer = new THREE.WebGLRenderTarget(size.width, size.height, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.FloatType,
stencilBuffer: false
});
this.writeBuffer = this.readBuffer.clone();
}
swap() {
const temp = this.readBuffer;
this.readBuffer = this.writeBuffer;
this.writeBuffer = temp;
}
render(scene, camera, toScreen = false) {
if (toScreen) {
this.renderer.render(scene, camera);
} else {
this.renderer.setRenderTarget(this.writeBuffer);
this.renderer.clear();
this.renderer.render(scene, camera)
this.renderer.setRenderTarget(null);
}
this.swap();
}
}
document.addEventListener('DOMContentLoaded', () => {
(new App()).start();
});
body {
margin: 0;
}
canvas {
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.121.1/build/three.js"></script>

Related

How to implement this shadertoy in three.js?

https://www.shadertoy.com/view/4tfXzl
Fragment shader with minor changes:
uniform sampler2D u_texture;
uniform float u_time;
float noise( in vec2 x ) {
vec2 p = floor(x);
vec2 f = fract(x);
vec2 uv = p.xy + f.xy*f.xy*(3.0-2.0*f.xy);
return texture( u_texture, (uv+118.4)/256.0, -100.0 ).x;
}
float fbm( vec2 x) {
float h = 0.0;
for (float i=1.0;i<10.0;i++) {
h+=noise(x*pow(1.6, i))*0.9*pow(0.6, i);
}
return h;
}
float warp(vec2 p, float mm) {
float m = 4.0;
vec2 q = vec2(fbm(vec2(p)), fbm(p+vec2(5.12*u_time*0.01, 1.08)));
vec2 r = vec2(fbm((p+q*m)+vec2(0.1, 4.741)), fbm((p+q*m)+vec2(1.952, 7.845)));
m /= mm;
return fbm(p+r*m);
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
fragCoord+=vec2(u_time*100.0, 0.0);
float col = warp(fragCoord*0.004, 12.0+fbm(fragCoord*0.005)*16.0);
gl_FragColor = mix(vec4(0.2, 0.4, 1.0, 1.0), vec4(1.0), smoothstep(0.0, 1.0, col));
}
Then i create simple plane in three.js and update u_time in the tick function, but the result is just blue screen. What do i do wrong?
Try it like so:
let camera, scene, renderer;
let uniforms;
init();
animate();
function init() {
camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
scene = new THREE.Scene();
const geometry = new THREE.PlaneGeometry(2, 2);
uniforms = {
u_time: {
value: 1.0
},
u_texture: {
value: new THREE.TextureLoader().load('https://i.imgur.com/BwLDhLB.png')
}
};
const material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
uniforms['u_time'].value = performance.now() / 1000;
renderer.render(scene, camera);
}
body {
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.130.1/build/three.min.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = vec4( position, 1.0 );
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
uniform sampler2D u_texture;
uniform float u_time;
float noise( in vec2 x ) {
vec2 p = floor(x);
vec2 f = fract(x);
vec2 uv = p.xy + f.xy*f.xy*(3.0-2.0*f.xy);
return texture( u_texture, (uv+118.4)/256.0, -100.0 ).x;
}
float fbm( vec2 x) {
float h = 0.0;
for (float i=1.0;i<10.0;i++) {
h+=noise(x*pow(1.6, i))*0.9*pow(0.6, i);
}
return h;
}
float warp(vec2 p, float mm) {
float m = 4.0;
vec2 q = vec2(fbm(vec2(p)), fbm(p+vec2(5.12*u_time*0.01, 1.08)));
vec2 r = vec2(fbm((p+q*m)+vec2(0.1, 4.741)), fbm((p+q*m)+vec2(1.952, 7.845)));
m /= mm;
return fbm(p+r*m);
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
fragCoord+=vec2(u_time*100.0, 0.0);
float col = warp(fragCoord*0.004, 12.0+fbm(fragCoord*0.005)*16.0);
gl_FragColor = mix(vec4(0.2, 0.4, 1.0, 1.0), vec4(1.0), smoothstep(0.0, 1.0, col));
}
</script>

I Can’t get derivatives (dFdx) working in my shader with WebGL1 or 2

I hope someone can help me with this. I am at the end of my rope, having gone through all of the discussions and examples I have found and still can’t get dFdx working, neither for WebGl1 or WebGL2. Partial code is shown below.
Thanks for you help.
The fragment shader uses:
#extension GL_OES_standard_derivatives : enable
precision highp float;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vViewPosition;
uniform vec3 color;
uniform float animateRadius;
uniform float animateStrength;
vec3 faceNormal(vec3 pos) {
vec3 fdx = vec3(dFdx(pos.x), dFdx(pos.y), dFdx(pos.z));
vec3 fdy = vec3(dFdy(pos.x), dFdy(pos.y), dFdy(pos.z));
//vec3 fdx = dFdx(pos);
//vec3 fdy = dFdy(pos);
return normalize(cross(fdx, fdy));
}
The console shows the following:
THREE.WebGLProgram: shader error: 0 35715 false gl.getProgramInfoLog Must have an compiled fragment shader attached. <empty string> THREE.WebGLShader: gl.getShaderInfoLog() fragment
WARNING: 0:2: 'GL_OES_standard_derivatives' : extension is not supported
ERROR: 0:14: 'GL_OES_standard_derivatives' : extension is not supported
ERROR: 0:14: 'GL_OES_standard_derivatives' : extension is not supported
ERROR: 0:14: 'GL_OES_standard_derivatives' : extension is not supported
ERROR: 0:15: 'GL_OES_standard_derivatives' : extension is not supported
ERROR: 0:15: 'GL_OES_standard_derivatives' : extension is not supported
ERROR: 0:15: 'GL_OES_standard_derivatives' : extension is not supported1: #define lengthSegments 300.0
2: #extension GL_OES_standard_derivatives : enable
3: precision highp float;
4:
5: varying vec3 vNormal;
6: varying vec2 vUv;
7: varying vec3 vViewPosition;
Here is also part of the Javascript:
module.exports = function (app) {
const totalMeshes = isMobile ? 30 : 40;
const isSquare = false;
const subdivisions = isMobile ? 200 : 300;
const numSides = isSquare ? 4 : 8;
const openEnded = false;
const geometry = createTubeGeometry(numSides, subdivisions, openEnded);
// add to a parent container
const container = new THREE.Object3D();
const lines = [];
//lines.length = 0;
ShaderLoader("scripts/Grp3D/Phys4646A2/tube.vert", "scripts/Grp3D/Phys4646A2/tube.frag", function (vertex, fragment) {
const baseMaterial = new THREE.RawShaderMaterial({
vertexShader: vertex,
fragmentShader: fragment,
side: THREE.FrontSide,
extensions: {
derivatives: true
},
defines: {
lengthSegments: subdivisions.toFixed(1),
ROBUST: false,
ROBUST_NORMALS: false,
FLAT_SHADED: isSquare
},
uniforms: {
thickness: { type: 'f', value: 1 },
time: { type: 'f', value: 0 },
color: { type: 'c', value: new THREE.Color('#303030') },
animateRadius: { type: 'f', value: 0 },
animateStrength: { type: 'f', value: 0 },
index: { type: 'f', value: 0 },
totalMeshes: { type: 'f', value: totalMeshes },
radialSegments: { type: 'f', value: numSides },
wavelength: { type: 'f', value: 2.0 }
}
});
for( var i = 0; i < totalMeshes; i++){
var t = totalMeshes <= 1 ? 0 : i / (totalMeshes - 1);
var material = baseMaterial.clone();
material.uniforms = THREE.UniformsUtils.clone(material.uniforms);
material.uniforms.index.value = t;
material.uniforms.thickness.value = randomFloat(0.005, 0.0075);
material.uniforms.wavelength.value = 2.0;
var mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = false;
lines.push(mesh);
container.add(mesh);
}
});
return {
object3d: container,
update,
setPalette
};
function update (dt,wvl) {
dt = dt / 1000;
lines.forEach(mesh => {
//console.log(dt);
mesh.material.uniforms.time.value += dt;
mesh.material.uniforms.wavelength.value = wvl;
});
}
};
...
In WebGL2 with version 300 es shaders they are supported by default, no extension needed.
<canvas></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/build/three.module.js';
const canvas = document.querySelector('canvas');
const renderer = new THREE.WebGLRenderer({canvas});
const material = new THREE.RawShaderMaterial({
vertexShader: `#version 300 es
in vec4 position;
out vec4 vPosition;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * position;
vPosition = gl_Position;
}
`,
fragmentShader: `#version 300 es
precision mediump float;
in vec4 vPosition;
out vec4 outColor;
void main() {
outColor = vec4(normalize(vec3(dFdx(vPosition.x), dFdy(vPosition.y), 0)) * 0.5 + 0.5, 1);
}
`,
});
const geo = new THREE.BoxBufferGeometry();
const mesh = new THREE.Mesh(geo, material);
const scene = new THREE.Scene();
scene.add(mesh);
mesh.rotation.set(0.4, 0.4, 0);
const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 10);
camera.position.z = 3;
renderer.render(scene, camera);
</script>
Note:
const fs = `#version 300 es
...
`;
Has #version 300 es as the first line
const fs = `
#version 300 es
...
`;
Has #version 300 es as the 2nd line (error)
With WebGL1 your code should work but three.js auto-chooses WebGL2 if it exists. To test WebGL1 we could force it to WebGL1 by creating the WebGL context ourselves,
const canvas = document.querySelector(selectorForCanvas);
const context = canvas.getContext('webgl');
const renderer = new THREE.WebGLRenderer({canvas, context});
Or, we can use a helper script to effectively disable webgl2
<script src="https://greggman.github.io/webgl-helpers/webgl2-disable.js"></script>
<canvas></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r122/build/three.module.js';
const canvas = document.querySelector('canvas');
const renderer = new THREE.WebGLRenderer({canvas});
const material = new THREE.RawShaderMaterial({
vertexShader: `
attribute vec4 position;
varying vec4 vPosition;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * position;
vPosition = gl_Position;
}
`,
fragmentShader: `
#extension GL_OES_standard_derivatives : enable
precision mediump float;
varying vec4 vPosition;
void main() {
gl_FragColor = vec4(normalize(vec3(dFdx(vPosition.x), dFdy(vPosition.y), 0)) * 0.5 + 0.5, 1);
}
`,
});
const geo = new THREE.BoxBufferGeometry();
const mesh = new THREE.Mesh(geo, material);
const scene = new THREE.Scene();
scene.add(mesh);
mesh.rotation.set(0.4, 0.4, 0);
const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 10);
camera.position.z = 3;
renderer.render(scene, camera);
</script>
Otherwise you can look at renderer.capabilities.isWebGL2 to see if three.js choose WebGL2 and adjust your shaders as appropriate.
As for why your code used to work and doesn't work now, two ideas:
your machine didn't support WebGL2 before and now it does
you upgraded three.js to a version that chooses WebGL2 automatically and the previous version you were using didn't.

How to make smooth transition in Three.js with shaders?

I have a basic Three.js app which just adds two textures to the plane. I pass them as uniforms to my shaders where I transform one to another when my mouse enters the plane.
And my question is is there a way to add a transition to this process of changing one texture to another?
So they change more smoothly than right now:
My fragment shader:
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
uniform float u_progress;
uniform sampler2D image;
uniform sampler2D displacementTexture;
uniform int mouseIntersects;
varying vec2 vUv;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec4 mainTexture = texture2D(image, vUv);
vec4 displacementTexture = texture2D(displacementTexture, vUv.yx);
vec2 disaplacedUv = vec2(
vUv.x,
vUv.y
);
if (mouseIntersects == 1) {
disaplacedUv.y = mix(vUv.y, displacementTexture.r + 0.2, 0.2);
gl_FragColor = texture2D(image, disaplacedUv);
} else {
gl_FragColor = mainTexture;
}
}
Three.js setup
const setup = () => {
...scene, camera etc
uniforms = {
image: { type: 't', value: new THREE.TextureLoader().load(whale) },
displacementTexture: { type: 't', value: new THREE.TextureLoader().load(texture) },
mouseIntersects: { type: 'f', value: 0 },
};
material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
uniforms: uniforms,
vertexShader: vertex,
fragmentShader: fragment,
});
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32);
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
raycaster = new THREE.Raycaster();
document.addEventListener('mousemove', (event) => updateMousePositions(event), false);
};
const updateMousePositions = (event) => {
event.preventDefault();
uniforms.u_mouse.value.x = (event.clientX / window.innerWidth) * 2 - 1;
uniforms.u_mouse.value.y = -(event.clientY / window.innerHeight) * 2 + 1;
};
const animate = () => {
requestAnimationFrame(animate);
render();
};
const render = () => {
uniforms.u_time.value += 0.05;
camera.lookAt(scene.position);
camera.updateMatrixWorld();
raycaster.setFromCamera(uniforms.u_mouse.value, camera);
const intersects = raycaster.intersectObject(mesh);
if (intersects.length === 1) {
uniforms.mouseIntersects.value = 1;
} else {
uniforms.mouseIntersects.value = 0;
}
renderer.render(scene, camera);
};
setup();
animate();
Instead of using uniform int mouseIntersects; make it a float so you can transition it from 0.0 to 1.0 in a smooth gradient instead of a hard discrete step. Then you could use the mix() GLSL command in your shader to smoothly transition from one texture to the other:
uniform sampler2D image;
uniform sampler2D displacementTexture;
uniform int mouseIntersects;
varying vec2 vUv;
void main() {
vec4 mainTexture = texture2D(image, vUv);
vec4 altTexture = texture2D(displacementTexture, vUv);
// 0.0 outputs mainTexture, 1.0 outputs altTexture
gl_FragColor = mix(mainTexture, altTexture, mouseIntersects);
}
Finally, in JavaScript, you can interpolate this value with MathUtils.lerp() when the mouseover takes place:
var targetValue = 0;
const render = () => {
// ...
if (intersects.length === 1) {
targetValue = 1;
} else {
targetValue = 0;
}
// This will smoothly transition from its current value to the targetValue on each frame:
uniforms.mouseIntersects.value = THREE.MathUtils.lerp(uniforms.mouseIntersects.value, targetValue, 0.1);
renderer.render(scene, camera);
};
... or you could use an animation library like GSAP that might give you more control

Preventing Texture Border Strectching in WebGL when Applying a Wave Effect with a Fragment Shader

I'm using GLSL via Three.js in my project.
I've got a flag texture like so:
When I apply a Fragment Shader to give it a wavy effect, the borders of the texture seem to stretch. However, I don't want that; I want the background color to be seen (in this case, clear) so that it just looks like a flag that's waving. You can see the problem in the CodePen here.
For reference (and if the CodePen for some reason doesn't work in the future), here's the code:
The Vertex Shader
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
The Fragment Shader
uniform vec2 u_resolution;
uniform float u_time;
uniform sampler2D image;
uniform vec2 strength;
uniform float speed;
varying vec2 vUv;
vec2 sineWave( vec2 p ) {
float x = strength.x * sin(-1.0 * p.y + 50.0 * p.x + u_time * speed);
float y = strength.y * sin(p.y + 10.0 * p.x + u_time * speed);
return vec2(p.x, p.y + y);
}
void main() {
vec4 color = texture2D(image, sineWave(vUv));
gl_FragColor = color;
}
Three.js Code
const CANVAS_WIDTH = 250;
const CANVAS_HEIGHT = 250;
const imageDataUri = 'data:image/png;base64,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';
const vertexShader = () => {
return `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
};
const fragmentShader = () => {
return `
uniform vec2 u_resolution;
uniform float u_time;
uniform sampler2D image;
uniform vec2 strength;
uniform float speed;
varying vec2 vUv;
vec2 sineWave( vec2 p ) {
float x = strength.x * sin(-1.0 * p.y + 50.0 * p.x + u_time * speed);
float y = strength.y * sin(p.y + 10.0 * p.x + u_time * speed);
return vec2(p.x, p.y + y);
}
void main() {
vec4 color = texture2D(image, sineWave(vUv));
gl_FragColor = color;
}
`
};
function init() {
let loader = new THREE.TextureLoader();
loader.load(
imageDataUri,
(texture) => {
let scene = new THREE.Scene();
let camera =
new THREE.PerspectiveCamera(75, CANVAS_WIDTH / CANVAS_HEIGHT, 0.1, 1000);
let renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setClearColor(0x000000, 0);
renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);
document.body.appendChild(renderer.domElement);
let uniforms = {
u_time: { type: 'f', value: 0.1 },
u_resolution: { type: 'v2', value: new THREE.Vector2() },
image: { type: 't', value: texture },
strength: { type: 'v2', value: new THREE.Vector2(0.01, 0.025) },
speed: { type: 'f', value: 8.0 }
};
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader: vertexShader(),
fragmentShader: fragmentShader()
});
let triangleMesh = new THREE.Mesh(new THREE.PlaneGeometry(CANVAS_WIDTH, CANVAS_HEIGHT, 1, 1), material);
scene.add(triangleMesh);
camera.position.z = 250;
let clock = new THREE.Clock();
let animate = () => {
requestAnimationFrame(animate);
triangleMesh.material.uniforms.u_time.value += clock.getDelta();
renderer.render(scene, camera);
};
animate();
},
undefined,
(err) => { console.error('Could not load texture', err) }
);
}
init();
body {background: orange;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>
What you can do is to scale the wave effect dependent on the v coordinate of the texture. If you scale by 1.0-p.y then the wave effect is zero at the top border and has its maximum at the bottom.
float y = strength.y * sin(p.y + 10.0 * p.x + u_time * speed) * (1.0-p.y*p.y);
For a less weak wave effect in the top area of the flag, scale by the square of the v coordinate ((1.0-p.y*p.y)):
float y = strength.y * sin(p.y + 10.0 * p.x + u_time * speed) * (1.0-p.y*p.y);
const CANVAS_WIDTH = 250;
const CANVAS_HEIGHT = 250;
const imageDataUri = 'data:image/png;base64,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';
const vertexShader = () => {
return `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
};
const fragmentShader = () => {
return `
uniform vec2 u_resolution;
uniform float u_time;
uniform sampler2D image;
uniform vec2 strength;
uniform float speed;
varying vec2 vUv;
vec2 sineWave( vec2 p ) {
float x = strength.x * sin(-1.0 * p.y + 50.0 * p.x + u_time * speed);
float y = strength.y * sin(p.y + 10.0 * p.x + u_time * speed) * (1.0-p.y*p.y);
return vec2(p.x, p.y + y);
}
void main() {
vec4 color = texture2D(image, sineWave(vUv));
gl_FragColor = color;
}
`
};
function init() {
let loader = new THREE.TextureLoader();
loader.load(
imageDataUri,
(texture) => {
let scene = new THREE.Scene();
let camera =
new THREE.PerspectiveCamera(75, CANVAS_WIDTH / CANVAS_HEIGHT, 0.1, 1000);
let renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setClearColor(0x000000, 0);
renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);
document.body.appendChild(renderer.domElement);
let uniforms = {
u_time: { type: 'f', value: 0.1 },
u_resolution: { type: 'v2', value: new THREE.Vector2() },
image: { type: 't', value: texture },
strength: { type: 'v2', value: new THREE.Vector2(0.01, 0.025) },
speed: { type: 'f', value: 8.0 }
};
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader: vertexShader(),
fragmentShader: fragmentShader()
});
let triangleMesh = new THREE.Mesh(new THREE.PlaneGeometry(CANVAS_WIDTH, CANVAS_HEIGHT, 1, 1), material);
scene.add(triangleMesh);
camera.position.z = 250;
let clock = new THREE.Clock();
let animate = () => {
requestAnimationFrame(animate);
triangleMesh.material.uniforms.u_time.value += clock.getDelta();
renderer.render(scene, camera);
};
animate();
},
undefined,
(err) => { console.error('Could not load texture', err) }
);
}
init();
body {background: orange;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>

How to reduce three.js CPU/GPU usage in browser

At the moment I have an animated globe which rotates and the the dots on the globe randomly change colour. It works fine but if left in the background it slows down my laptop a lot. Are there any changes I could make that would reduce how much memory it is using?
In the task manager on chrome I can see it's using 12% CPU and 128MB of GPU memory when the tab is active. Is that normal for three.js or does the code need to be changed?
ngAfterViewInit() {
if(this.enabled) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.rotateSpeed = 0.5;
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.5;
this.controls.rotationSpeed = 0.3;
this.controls.enableZoom = false;
this.controls.autoRotate = true;
this.controls.autoRotateSpeed = -1;
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
this.animate();
const timerId = setInterval(() => this.updateColor(), 650);
}
}
private get enabled(): boolean {
if(this._enabled!==undefined) {
return this._enabled;
}
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
this._enabled = gl && gl instanceof WebGLRenderingContext;
return this._enabled;
}
private initGlobe(): void {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000);
this.camera.position.set(0, 5, 15);
this.camera.lookAt(this.scene.position);
this.renderer = new THREE.WebGLRenderer({
antialias: true
});
this.renderer.setClearColor('rgb(55, 44, 80)');
this.geom = new THREE.SphereBufferGeometry(6, 350, 90);
this.colors = [];
this.color = new THREE.Color();
this.colorList = ['rgb(123, 120, 194)'];
for (let i = 0; i < this.geom.attributes.position.count; i++) {
this.color.set(this.colorList[THREE.Math.randInt(0, this.colorList.length - 1)]);
this.color.toArray(this.colors, i * 3);
}
this.geom.addAttribute('color', new THREE.BufferAttribute(new Float32Array(this.colors), 3));
this.geom.addAttribute('colorRestore', new THREE.BufferAttribute(new Float32Array(this.colors), 3));
this.loader = new THREE.TextureLoader();
this.loader.setCrossOrigin('');
this.texture = this.loader.load('/assets/globe-dot.jpg');
this.texture.wrapS = THREE.RepeatWrapping;
this.texture.wrapT = THREE.RepeatWrapping;
this.texture.repeat.set(1, 1);
const oval = this.loader.load('/assets/circle.png');
this.points = new THREE.Points(this.geom, new THREE.ShaderMaterial({
vertexColors: THREE.VertexColors,
uniforms: {
visibility: {
value: this.texture
},
shift: {
value: 0
},
shape: {
value: oval
},
size: {
value: 0.4
},
scale: {
value: 300
}
},
vertexShader: `
uniform float scale;
uniform float size;
varying vec2 vUv;
varying vec3 vColor;
void main() {
vUv = uv;
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4( position, 0.99 );
gl_PointSize = size * ( scale / length( mvPosition.xyz )) * (0.3 + sin(uv.y * 3.1415926) * 0.35 );
gl_Position = projectionMatrix * mvPosition;
}
// `,
fragmentShader: `
uniform sampler2D visibility;
uniform float shift;
uniform sampler2D shape;
varying vec2 vUv;
varying vec3 vColor;
void main() {
vec2 uv = vUv;
uv.x += shift;
vec4 v = texture2D(visibility, uv);
if (length(v.rgb) > 1.0) discard;
gl_FragColor = vec4( vColor, 0.9 );
vec4 shapeData = texture2D( shape, gl_PointCoord );
if (shapeData.a < 0.0625) discard;
gl_FragColor = gl_FragColor * shapeData;
}
`,
transparent: false
}));
this.points.sizeAttenuation = false;
this.scene.add(this.points);
this.globe = new THREE.Mesh(this.geom, new THREE.MeshBasicMaterial({
color: 'rgb(65, 54, 88)', transparent: true, opacity: 0.5
}));
this.globe.scale.setScalar(0.99);
this.points.add(this.globe);
this.scene.add(this.globe);
}
animate() {
this.controls.update();
this.renderer.render(this.scene, this.camera);
this.animationQueue.push(this.animate);
window.requestAnimationFrame(_ => this.nextAnimation());
}
nextAnimation() {
try {
const animation = this.animationQueue.shift();
if (animation instanceof Function) {
animation.bind(this)();
}
} catch (e) {
console.error(e);
}
}
updateColor() {
for (let i = 0; i < this.usedIndices.length; i++) {
let idx = this.usedIndices[i];
this.geom.attributes.color.copyAt(idx, this.geom.attributes.colorRestore, idx);
}
for (let i = 0; i < this.pointsUsed; i++) {
let idx = THREE.Math.randInt(0, this.geom.attributes.color.count - 1);
if (idx%5 == 0 && idx%1 == 0) {
this.geom.attributes.color.setXYZ(idx, 0.9, 0.3, 0);
}
else {
this.geom.attributes.color.setXYZ(idx, 1, 1, 1);
}
this.usedIndices[i] = idx;
}
this.geom.attributes.color.needsUpdate = true;
I looked at other questions which suggest merging the meshes but I'm not sure that would work here. Thanks!
It depends on what you mean by "background"
If by "background" you mean "not the front tab" then, if you're using requestAnimationFrame (which you are) then if your page is not the front tab of the browser or if you minimize the browser window the browser will stop sending you animation frame events and your page should stop completely.
If by "background" you mean the front tab but of a window that's not minimized and is also not the front window then you can use the blur and focus events to stop the page completely.
Example: NOTE: blur events don't seem to work in an iframe so it won't work in the snippet below but if you copy it to a file it should work
let requestId;
function start() {
if (!requestId) {
requestId = requestAnimationFrame(animate);
}
}
function stop() {
console.log('stop');
if (requestId) {
cancelAnimationFrame(requestId);
requestId = undefined;
}
}
const ctx = document.querySelector("canvas").getContext('2d');
function animate(time) {
requestId = undefined;
ctx.save();
ctx.translate(
150 + 150 * Math.cos(time * 0.001),
75 + 75 * Math.sin(time * 0.003),
);
ctx.scale(
Math.cos(time * 0.005),
Math.cos(time * 0.007),
);
ctx.fillStyle = `hsl(${time % 360},100%,50%)`;
ctx.fillRect(-50, 50, 100, 100);
ctx.restore();
start();
}
start();
window.addEventListener('blur', stop);
window.addEventListener('focus', start);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas>
Of course rather than stopping completely on blur you could throttle your app your self. Only render every 5th frame or render less things, etc...

Resources