I'm a newbie to ThreeJS and I have a Shadertoy shader (this one) that I want to embed in my webpage. It makes use of a buffer. I haven't been able to find much useful info on the topic. The main image shader applies a heatmap color scheme, while the buffer shader uses some math to solve the 2D heat equation, which is what the Shadertoy shader does. The buffer shader uses its previous state in these calculations. For reference, I followed the instructions in this Stackoverflow post.
Here's the main image:
out vec4 col;
uniform sampler2D tex;
void main()
{
float t = texelFetch(tex, ivec2(gl_FragCoord.xy), 0).x;
col = vec4(
sqrt(t),
t * t * t,
max(sin(6.283 * t), 0.),
t
);
}
And here's the buffer:
#define R 8.
#define DT 0.1
out vec4 col;
uniform sampler2D tex;
uniform float alpha;
uniform vec2 iMouse;
vec4 laplace(vec2 p)
{
// 5-point stencil Laplacian
vec4 c = texelFetch(tex, ivec2(p), 0);
vec4 lt = texelFetch(tex, ivec2(p + vec2(-1, 0)), 0);
vec4 rt = texelFetch(tex, ivec2(p + vec2(1, 0)), 0);
vec4 up = texelFetch(tex, ivec2(p + vec2(0, -1)), 0);
vec4 dn = texelFetch(tex, ivec2(p + vec2(0, 1)), 0);
return lt + up + rt + dn - (4. * c);
}
void main()
{
vec2 p = gl_FragCoord.xy;
if (distance(p, iMouse) < R)
{
col = vec4(1.0);
} else {
// Euler integration
vec4 T = texelFetch(tex, ivec2(p), 0);
vec4 iT = alpha * laplace(p);
col = T + iT * DT;
}
}
Here's my JS code:
let canvas, renderer, camera, renderTarg;
let size;
let count = 2;
let scene0, plane0, fragment0, uniforms0;
let scene1, plane1, fragment1, uniforms1;
let drawing = false;
let a = 1;
let coord = [200, 200];
document.addEventListener("DOMContentLoaded", function() {
// Set up canvas and renderers
canvas = document.getElementById("canv");
canvas.addEventListener("mousedown", startDraw);
canvas.addEventListener("mousemove", draw);
canvas.addEventListener("mouseup", endDraw);
canvas.addEventListener("mouseout", endDraw);
size = window.innerHeight;
renderer = new THREE.WebGLRenderer({
canvas,
preserveDrawingBuffer: true
});
// Initialize objects and cameras
scene0 = new THREE.Scene();
scene1 = new THREE.Scene();
plane0 = new THREE.PlaneGeometry(2, 2);
plane1 = new THREE.PlaneGeometry(2, 2);
renderTarg = new THREE.WebGLRenderTarget(size, size);
// Load shaders
let loader = new THREE.FileLoader();
function next() {
count--;
if (count == 0)
load();
}
loader.load("static/buffer.frag", (dat) => {fragment0 = dat; next();});
loader.load("static/mainShader.frag", (dat) => {fragment1 = dat; next();});
});
function load() {
// Create meshes and apply shaders
uniforms0 = {tex: {value: new THREE.Texture()}, iMouse: {value: new THREE.Vector2()}, alpha: {value: a}};
uniforms1 = {tex: {value: new THREE.Texture()}};
let mat0 = new THREE.ShaderMaterial({fragmentShader: fragment0, uniforms: uniforms0, glslVersion: THREE.GLSL3});
let mat1 = new THREE.ShaderMaterial({fragmentShader: fragment1, uniforms: uniforms1, glslVersion: THREE.GLSL3});
let planeMesh0 = new THREE.Mesh(plane0, mat0);
let planeMesh1 = new THREE.Mesh(plane1, mat1);
scene0.add(planeMesh0);
scene1.add(planeMesh1);
camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
renderer.setSize(size, size, false);
animLoop();
}
function animLoop() {
// Render buffer
uniforms0.tex.value = renderTarg.texture;
uniforms0.iMouse.value.set(coord[0], coord[1]);
uniforms0.alpha.value = a;
renderer.render(scene0, camera, renderTarg);
// Render main
uniforms1.tex.value = renderTarg.texture;
renderer.render(scene1, camera);
window.requestAnimationFrame(animLoop);
}
/*
Event handlers for clicking and dragging, to trace path.
*/
const startDraw = (e) => {
drawing = true;
draw(e);
}
const draw = (e) => {
// Draw on canvas
if (drawing) {
let x = e.pageX - canvas.offsetLeft;
let y = size - (e.pageY - canvas.offsetTop);
coord = [x, y];
}
}
const endDraw = (e) => {
drawing = false;
}
When I run it I just get a black screen, no error messages. When I comment out the second call to renderer.render, I get a white circle that follows the cursor.
Thanks in advance.
Related
I'm trying to replace all RGB pixels with the value of 0 to 1 (out of the max value of 255).
Here is my code on stackblitz.
You can see that after I'm reassigning the buffer with the new pixels some of the pixles are back to 0. On Firefox it's actually works with this image:
But won't work with the full size image:
It's like the browser won't allow a certain contrast of something like that.
Sharing my code here as well:
const getBase64FromFile = async (file: File): Promise<string> => {
return new Promise((resolve: Function, reject: Function) => {
let reader = new FileReader();
reader.addEventListener(
'load',
(arg) => {
resolve(reader.result);
},
false
);
reader.readAsDataURL(file);
});
};
// Returns the amount of pixels with RGB 0 value
const howManyZeros = async (src: string): Promise<number> => {
return new Promise((resolve: Function, reject: Function) => {
const image = new Image();
image.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image, 0, 0);
const data = ctx.getImageData(
0,
0,
image.naturalWidth,
image.naturalHeight
).data;
let zeros = 0;
for (var i = 0; i < data.length; i += 4) {
if (data[i] === 0) zeros++;
if (data[i + 1] === 0) zeros++;
if (data[i + 2] === 0) zeros++;
}
resolve(zeros);
};
image.src = src;
});
};
const onFinish = async (src: string) => {
document.querySelector(
'p#after'
).textContent = `nunber of zeros after: ${await howManyZeros(src)}`;
(document.querySelector('img#after-img') as HTMLImageElement).src = src;
const a = document.querySelector('a');
a.setAttribute('href', src);
a.setAttribute('download', 'image.png');
a.style.display = '';
};
const onFileChange = async (e: Event | any) => {
const image = new Image();
image.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
let data = ctx.getImageData(
0,
0,
image.naturalWidth,
image.naturalHeight
).data;
let buffer = new Uint8ClampedArray(
image.naturalWidth * image.naturalHeight * 4
);
// Iterate over all the pixels and increase all RGB 0 values to 1
for (var i = 0; i < data.length; i += 4) {
if (data[i] === 0) buffer[i] = 1;
else buffer[i] = data[i];
if (data[i + 1] === 0) buffer[i + 1] = 1;
else buffer[i + 1] = data[i + 1];
if (data[i + 2] === 0) buffer[i + 2] = 1;
else buffer[i + 2] = data[i + 2];
buffer[i + 3] = data[i + 3];
}
const iData = ctx.createImageData(image.naturalWidth, image.naturalHeight);
iData.data.set(buffer);
ctx.putImageData(iData, 0, 0);
onFinish(canvas.toDataURL('image/png', 1));
};
let src = await getBase64FromFile(e.target.files[0]);
document.querySelector(
'p#before'
).textContent = `nunber of zeros before: ${await howManyZeros(src)}`;
(document.querySelector('img#before-img') as HTMLImageElement).src = src;
image.src = src;
};
const input: HTMLInputElement = document.querySelector('input');
input.addEventListener('change', onFileChange, false);
Appreciate any help with this and praying that's not a browser issue but something with my code.
Due to canvas spec, which doesn't guarantee that the pixels stay the same as you set them, you can't use browser built in image manipulation functions.
Due to the lossy nature of converting between color spaces and converting to and from premultiplied alpha color values, pixels that have just been set using putImageData(), and are not completely opaque, might be returned to an equivalent getImageData() as different values.
In your case pixel values with high transparency get turned to 0 again.
This doesn't happen in WebGL context with the premultipliedAlpha context attribute set to false, but the solution involves a lot of code.
The following code is based on the example from WebGL2 Fundamentals:
const vertexShaderSource = `#version 300 es
// an attribute is an input (in) to a vertex shader.
// It will receive data from a buffer
in vec2 a_position;
in vec2 a_texCoord;
// Used to pass in the resolution of the canvas
uniform vec2 u_resolution;
// Used to pass the texture coordinates to the fragment shader
out vec2 v_texCoord;
// all shaders have a main function
void main() {
// convert the position from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
// pass the texCoord to the fragment shader
// The GPU will interpolate this value between points.
v_texCoord = a_texCoord;
}`;
const fragmentShaderSource = `#version 300 es
// fragment shaders don't have a default precision so we need
// to pick one. highp is a good default. It means "high precision"
precision highp float;
// our texture
uniform sampler2D u_image;
// the texCoords passed in from the vertex shader.
in vec2 v_texCoord;
// we need to declare an output for the fragment shader
out vec4 outColor;
void main() {
vec4 inColor = texture(u_image, v_texCoord);
outColor = vec4(
inColor.r != 0.0 ? inColor.r : 1.0/255.0,
inColor.g != 0.0 ? inColor.g : 1.0/255.0,
inColor.b != 0.0 ? inColor.b : 1.0/255.0,
inColor.a
);
}`;
function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function() {
resolve(this.result);
}
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
function loadImage(url) {
return new Promise(async(resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve(this);
}
image.onerror = reject;
image.src = url;
})
}
function canvasToBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob(blob => blob ? resolve(blob) : reject(canvas), "image/png");
});
}
function howManyZeros(gl) {
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
let data = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
gl.readPixels(0, 0, gl.canvas.width, gl.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
let zeros = 0;
for (let i=0; i<data.length; i++) {
if (i % 4 == 3) continue; // ignore alpha
if (data[i] == 0) zeros++;
}
return zeros;
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]), gl.STATIC_DRAW);
}
function render(image) {
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const gl = canvas.getContext("webgl2", {
premultipliedAlpha: false
});
if (!gl) {
console.error("No WebGL2");
return;
}
canvas.gl = gl;
// setup GLSL program
const program = webglUtils.createProgramFromSources(gl, [vertexShaderSource, fragmentShaderSource]);
// look up where the vertex data needs to go.
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
// lookup uniforms
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const imageLocation = gl.getUniformLocation(program, "u_image");
// Create a vertex array object (attribute state)
const vao = gl.createVertexArray();
// and make it the one we're currently working with
gl.bindVertexArray(vao);
// Create a buffer and put a single pixel space rectangle in
// it (2 triangles)
const positionBuffer = gl.createBuffer();
// Turn on the attribute
gl.enableVertexAttribArray(positionAttributeLocation);
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
let size = 2; // 2 components per iteration
let type = gl.FLOAT; // the data is 32bit floats
let normalize = false; // don't normalize the data
let stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
let offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset);
// provide texture coordinates for the rectangle.
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
// Turn on the attribute
gl.enableVertexAttribArray(texCoordAttributeLocation);
// Tell the attribute how to get data out of texCoordBuffer (ARRAY_BUFFER)
size = 2; // 2 components per iteration
type = gl.FLOAT; // the data is 32bit floats
normalize = false; // don't normalize the data
stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
texCoordAttributeLocation, size, type, normalize, stride, offset);
// Create a texture.
const texture = gl.createTexture();
// make unit 0 the active texture uint
// (ie, the unit all other texture commands will affect
gl.activeTexture(gl.TEXTURE0 + 0);
// Bind it to texture unit 0' 2D bind point
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we don't need mips and so we're not filtering
// and we don't repeat
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
const mipLevel = 0; // the largest mip
const internalFormat = gl.RGBA; // format we want in the texture
const srcFormat = gl.RGBA; // format of data we are supplying
const srcType = gl.UNSIGNED_BYTE; // type of data we are supplying
gl.texImage2D(gl.TEXTURE_2D,
mipLevel,
internalFormat,
srcFormat,
srcType,
image);
// Tell WebGL how to convert from clip space to pixels
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Tell it to use our program (pair of shaders)
gl.useProgram(program);
// Bind the attribute/buffer set we want.
gl.bindVertexArray(vao);
// Pass in the canvas resolution so we can convert from
// pixels to clipspace in the shader
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
// Tell the shader to get the texture from texture unit 0
gl.uniform1i(imageLocation, 0);
// Bind the position buffer so gl.bufferData that will be called
// in setRectangle puts data in the position buffer
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Set a rectangle the same size as the image.
setRectangle(gl, 0, 0, image.width, image.height);
// Draw the rectangle.
const primitiveType = gl.TRIANGLES;
offset = 0;
const count = 6;
gl.drawArrays(primitiveType, offset, count);
return canvas;
}
async function onFileChange(e) {
const png_data = await readFile(e.target.files[0]);
const png_blob = new Blob([png_data], {
type: 'image/png'
});
const png_url = URL.createObjectURL(png_blob);
document.querySelector("#before-img").src = png_url;
const image = await loadImage(png_url);
let canvas = render(image);
document.querySelector("#after").textContent = `nunber of zeros after: ${howManyZeros(canvas.gl)}`
const new_png_blob = await canvasToBlob(canvas);
const new_png_url = URL.createObjectURL(new_png_blob);
const dl_link = document.querySelector("a");
dl_link.href = new_png_url;
dl_link.style.display = "";
document.querySelector("#after-img").src = new_png_url;
};
const input = document.querySelector('input[type=file]');
input.addEventListener('change', onFileChange, false);
<input type="file" />
<div>
<a style="display: none" download="image.png">download</a>
</div>
<div>
<p id="before"></p>
<img id="before-img" src="" />
</div>
<div>
<p id="after"></p>
<img id="after-img" src="" />
</div>
<script src="https://webgl2fundamentals.org/webgl/resources/webgl-utils.js"></script>
Alternatively you could do it with a 3rd party image manupulation library.
Here's an example using the UPNG.js library:
function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function() {
resolve(this.result);
}
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
async function onFileChange(e) {
const png_data = await readFile(e.target.files[0]);
const png_blob = new Blob([png_data], {
type: 'image/png'
});
const png_url = URL.createObjectURL(png_blob);
document.querySelector("#before-img").src = png_url;
const png = UPNG.decode(png_data);
const png_frames = UPNG.toRGBA8(png);
const png_frame = new Uint8Array(png_frames[0]);
for (var i = 0; i < png_frame.length; i += 4) {
png_frame[i + 0] = png_frame[i + 0] == 0 ? 1 : png_frame[i + 0];
png_frame[i + 1] = png_frame[i + 1] == 0 ? 1 : png_frame[i + 1];
png_frame[i + 2] = png_frame[i + 2] == 0 ? 1 : png_frame[i + 2];
//png_frame[i+3] = 255; // remove transparency
}
const new_png_data = UPNG.encode([png_frame.buffer], png.width, png.height, 0);
const new_png_blob = new Blob([new_png_data], {
type: 'image/png'
});
const new_png_url = URL.createObjectURL(new_png_blob);
/*const new_png_url = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(new_png_data)));*/
const dl_link = document.querySelector("a");
dl_link.href = new_png_url;
dl_link.style.display = "";
document.querySelector("#after-img").src = new_png_url;
};
const input = document.querySelector('input[type=file]');
input.addEventListener('change', onFileChange, false);
<input type="file" />
<div>
<a style="display: none" download="image.png">download</a>
</div>
<div>
<p id="before"></p>
<img id="before-img" src="" />
</div>
<div>
<p id="after"></p>
<img id="after-img" src="" />
</div>
<script type="module">
import UPNG from "https://cdn.skypack.dev/upng-js#2.1.0"; window.UPNG = UPNG;
</script>
Here's an example based on the second live example from https://threejsfundamentals.org/threejs/lessons/threejs-shadertoy.html:
html, body {
height: 100%;
margin: 0;
}
#c {
width: 100%;
height: 100%;
display: block;
}
<canvas id="c"></canvas>
<script type="module">
// Three.js - Shadertoy Basic
// from https://threejsfundamentals.org/threejs/threejs-shadertoy-basic.html
import * as THREE from 'https://unpkg.com/three#0.122.0/build/three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
renderer.autoClearColor = false;
const camera = new THREE.PerspectiveCamera(
45, 16/9, 0.01, 1000
);
camera.position.z = 5
const scene = new THREE.Scene();
const plane = new THREE.PlaneBufferGeometry(2, 2);
const fragmentShader = `
#include <common>
uniform vec3 iResolution;
uniform float iTime;
// https://www.shadertoy.com/view/MtXSWj
float alternate(float p, float d){;
return sign(fract(p*d*.5)*2.-1.);
}
vec3 rainbow(float t){
return sin(t+vec3(0,.33,.66)*6.28)*.5+.5;
}
vec3 TwinDragon(vec2 p){
float time = fract(iTime*0.05)*20.;
//scaling
p = (p*2.-iResolution.xy)/iResolution.y*1.5;
//----------the fractal stuff---- ---THIS IS ANIMATIONS----(so remove them if you want)
p.y += alternate(p.x, 256. )/512. * clamp(time-16.,0.,2.)/2.;
p.x -= alternate(p.y, 128. )/256. * clamp(time-14.,0.,2.)/2.;
p.y += alternate(p.x, 64. )/128. * clamp(time-12.,0.,2.)/2.;
p.x -= alternate(p.y, 32. )/ 64. * clamp(time-10.,0.,2.)/2.;
p.y += alternate(p.x, 16. )/ 32. * clamp(time- 8.,0.,2.)/2.;
p.x -= alternate(p.y, 8. )/ 16. * clamp(time- 6.,0.,2.)/2.;
p.y += alternate(p.x, 4. )/ 8. * clamp(time- 4.,0.,2.)/2.;
p.x -= alternate(p.y, 2. )/ 4. * clamp(time- 2.,0.,2.)/2.;
// prettifying
vec2 block = ceil(p+.5); //index for blocks from which the fractal is shifted
vec3 color = rainbow(block.x*4.+block.y); //rainbow palette using block index as t
float dis = length(fract(p+.5)*2.-1.);//distance to middle of block
color *= .5+dis*.7; //using distance within block for some more pretty.
return color;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord ){
vec2 d = vec2(.5,0);
//some antialiasing
vec3 col = (
TwinDragon(fragCoord+d.xy)+
TwinDragon(fragCoord-d.xy)+
TwinDragon(fragCoord+d.yx)+
TwinDragon(fragCoord-d.yx)
)*.25;
fragColor = vec4(col,1.);
}
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`;
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3() },
};
const material = new THREE.ShaderMaterial({
fragmentShader,
uniforms,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(plane, material)
scene.add(mesh);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001; // convert to seconds
resizeRendererToDisplaySize(renderer);
const canvas = renderer.domElement;
uniforms.iResolution.value.set(canvas.width, canvas.height, 1);
uniforms.iTime.value = time;
mesh.rotation.y += 0.01
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>
But as you see, when I change to PerspectiveCamera, and rotate the plane, the texture does not transform with the object.
What's the simplest way to modify the example to make the shader transform with the plane, so that the graphic appears to be on the surface of the plane instead of the plane appearing to be like a mask?
The answer is to pass the uv coordinates from the vertex shader to the fragment shader using a varying variable.
Basically we can replace the lines
const fragment = `
... clipped ...
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`;
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3() },
};
const material = new THREE.ShaderMaterial({
fragmentShader,
with
const fragment = `
... clipped ...
varying vec2 vUv;
void main() {
mainImage(gl_FragColor, vUv * iResolution.xy);
}
`;
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3() },
};
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
and we get the following result:
html, body {
height: 100%;
margin: 0;
}
#c {
width: 100%;
height: 100%;
display: block;
}
<canvas id="c"></canvas>
<script type="module">
// Three.js - Shadertoy Basic
// from https://threejsfundamentals.org/threejs/threejs-shadertoy-basic.html
import * as THREE from 'https://unpkg.com/three#0.122.0/build/three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
renderer.autoClearColor = false;
const camera = new THREE.PerspectiveCamera(
45, 16/9, 0.01, 1000
);
camera.position.z = 5
const scene = new THREE.Scene();
const plane = new THREE.PlaneBufferGeometry(2, 2);
const fragmentShader = `
#include <common>
uniform vec3 iResolution;
uniform float iTime;
// https://www.shadertoy.com/view/MtXSWj
float alternate(float p, float d){;
return sign(fract(p*d*.5)*2.-1.);
}
vec3 rainbow(float t){
return sin(t+vec3(0,.33,.66)*6.28)*.5+.5;
}
vec3 TwinDragon(vec2 p){
float time = fract(iTime*0.05)*20.;
//scaling
p = (p*2.-iResolution.xy)/iResolution.y*1.5;
//----------the fractal stuff---- ---THIS IS ANIMATIONS----(so remove them if you want)
p.y += alternate(p.x, 256. )/512. * clamp(time-16.,0.,2.)/2.;
p.x -= alternate(p.y, 128. )/256. * clamp(time-14.,0.,2.)/2.;
p.y += alternate(p.x, 64. )/128. * clamp(time-12.,0.,2.)/2.;
p.x -= alternate(p.y, 32. )/ 64. * clamp(time-10.,0.,2.)/2.;
p.y += alternate(p.x, 16. )/ 32. * clamp(time- 8.,0.,2.)/2.;
p.x -= alternate(p.y, 8. )/ 16. * clamp(time- 6.,0.,2.)/2.;
p.y += alternate(p.x, 4. )/ 8. * clamp(time- 4.,0.,2.)/2.;
p.x -= alternate(p.y, 2. )/ 4. * clamp(time- 2.,0.,2.)/2.;
// prettifying
vec2 block = ceil(p+.5); //index for blocks from which the fractal is shifted
vec3 color = rainbow(block.x*4.+block.y); //rainbow palette using block index as t
float dis = length(fract(p+.5)*2.-1.);//distance to middle of block
color *= .5+dis*.7; //using distance within block for some more pretty.
return color;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord ){
vec2 d = vec2(.5,0);
//some antialiasing
vec3 col = (
TwinDragon(fragCoord+d.xy)+
TwinDragon(fragCoord-d.xy)+
TwinDragon(fragCoord+d.yx)+
TwinDragon(fragCoord-d.yx)
)*.25;
fragColor = vec4(col,1.);
}
varying vec2 vUv;
void main() {
mainImage(gl_FragColor, vUv * iResolution.xy);
}
`;
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3() },
};
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(plane, material)
scene.add(mesh);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001; // convert to seconds
resizeRendererToDisplaySize(renderer);
const canvas = renderer.domElement;
uniforms.iResolution.value.set(canvas.width, canvas.height, 1);
uniforms.iTime.value = time;
mesh.rotation.y += 0.01
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>
I'm basically trying to achieve a kaleidoscopic effect with just one side, but I'm working with lots of Points, so I'd like that to happen in the shader. However if there's a Threejs trick that mirrors half of the texture or the Points object, that would be great. I tried to apply transformation matrices but I can't get it to work.
I found an old KaleidoShader that requires the usage of EffectComposer, but I'd like to implement it manually myself (without EffectComposer) and I'm struggling to do so. I'm using an FBO and I tried adding the code from that shader in both my simulation and render shaders but it's having no effect at all. Do I have to add yet another FBO texture or is it possibile to do those calculations in one of the existing shaders?
For visual reference https://ma-hub.imgix.net/wp-images/2019/01/23205110/premiere-pro-mirror-effect.jpg
I've spent so much time without getting to the bottom of this, hopefully someone can point me in the right direction.
Thanks
I just followed this article
Pasting in the code from that repo seems to work
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';
import {EffectComposer} from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/examples/jsm/postprocessing/EffectComposer.js';
import {RenderPass} from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/examples/jsm/postprocessing/RenderPass.js';
import {ShaderPass} from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/examples/jsm/postprocessing/ShaderPass.js';
import {GUI} from 'https://threejsfundamentals.org/threejs/../3rdparty/dat.gui.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const scene = new THREE.Scene();
{
const color = 0xFFFFFF;
const intensity = 2;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({color});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.position.x = x;
return cube;
}
const cubes = [
makeInstance(geometry, 0x44aa88, 0),
makeInstance(geometry, 0x8844aa, -2),
makeInstance(geometry, 0xaa8844, 2),
];
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// from:
// https://github.com/mistic100/three.js-examples/blob/master/LICENSE
const kaleidoscopeShader = {
uniforms: {
"tDiffuse": { value: null },
"sides": { value: 6.0 },
"angle": { value: 0.0 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float sides;
uniform float angle;
varying vec2 vUv;
void main() {
vec2 p = vUv - 0.5;
float r = length(p);
float a = atan(p.y, p.x) + angle;
float tau = 2. * 3.1416 ;
a = mod(a, tau/sides);
a = abs(a - tau/sides/2.) ;
p = r * vec2(cos(a), sin(a));
vec4 color = texture2D(tDiffuse, p + 0.5);
gl_FragColor = color;
}
`
};
const kaleidoscopePass = new ShaderPass(kaleidoscopeShader);
kaleidoscopePass.renderToScreen = true;
composer.addPass(kaleidoscopePass);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
const gui = new GUI();
gui.add(kaleidoscopePass.uniforms.sides, 'value', 0, 20).name('sides');
gui.add(kaleidoscopePass.uniforms.angle, 'value', 0, 6.28, 0.01).name('angle');
let then = 0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
composer.setSize(canvas.width, canvas.height);
}
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1;
const rot = now * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
composer.render(deltaTime);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>
There is a texture wrap mode that does mirroring.
texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping
Does that help?
edit: Here's an example showing mirroredrepeatwrapping on both axis:
https://glitch.com/~three-mirroredrepeatwrapping
I want to rotate and translate a 2d shape made with signed distance functions.
The docs say this is the method:
vec3 opTx( in vec3 p, in transform t, in sdf3d primitive )
{
return primitive( invert(t)*p );
}
It looks to me like primitive is some kind of function (or a struct) I can call, Is there a way to pass functions like that (or how does this make sense)?
Firstly I don't know what transform and sdf3d types are, and what is the invert function. Secondly how do I apply this to 2d?
const fShaderSource = `#version 300 es
precision mediump float;
uniform vec2 u_resolution;
out vec4 outColor;
float sdLine( in vec2 p, in vec2 a, in vec2 b )
{
vec2 pa = p-a, ba = b-a;
float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
return length( pa - ba*h );
}
vec2 screenToWorld(vec2 screen) {
vec2 result = 2.0 * (screen/u_resolution.xy - 0.5);
result.x *= u_resolution.x/u_resolution.y;
return result;
}
void main() {
vec2 p = screenToWorld(gl_FragCoord.xy);
float sd = sdLine(p, vec2(0.0), vec2(0.0, 0.5));
vec3 col = vec3(0.0);
col += 1.0 - smoothstep(0.0, 0.04, abs(sd));
outColor = vec4(col, 1.0);
}
`;
const vShaderSource = `#version 300 es
precision mediump float;
in vec2 a_position;
uniform vec2 u_resolution;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
`;
main(document.getElementById('app'));
function main(element) {
const canvas = document.createElement('canvas'),
gl = canvas.getContext('webgl2');
element.append(canvas);
const displayWidth = canvas.clientWidth,
displayHeight = canvas.clientHeight;
canvas.width = displayWidth;
canvas.height = displayHeight;
let graphics = new Graphics({width: displayWidth, height: displayHeight}, gl);
new Loop(() => {
graphics.render();
}).start();
}
function Graphics(state, gl) {
const { width, height } = state;
let vShader = createShader(gl, gl.VERTEX_SHADER, vShaderSource);
let fShader = createShader(gl, gl.FRAGMENT_SHADER, fShaderSource);
let program = createProgram(gl, vShader, fShader);
let posAttrLocation = gl.getAttribLocation(program, "a_position");
let posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
/*
(-1, 1).( 1, 1)
.
(-1,-1).( 1,-1)
*/
let positions = [
-1, 1,
-1, -1,
1, -1,
-1, 1,
1,-1,
1, 1
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
let vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(posAttrLocation);
let size = 2,
type = gl.FLOAT,
normalize = false,
stride = 0,
offset = 0;
gl.vertexAttribPointer(posAttrLocation,
size,
type,
normalize,
stride,
offset);
let resUniformLocation = gl.getUniformLocation(program, "u_resolution");
gl.clearColor(0, 0, 0, 0);
this.render = () => {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.uniform2f(resUniformLocation, gl.canvas.width, gl.canvas.height);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 6);
};
}
function createShader(gl, type, source) {
let shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
};
function createProgram(gl, vShader, fShader) {
let program = gl.createProgram();
gl.attachShader(program, vShader);
gl.attachShader(program, fShader);
gl.linkProgram(program);
let success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
// Loop Library
function Loop(fn) {
const perf = window.performance !== undefined ? window.performance : Date;
const now = () => perf.now();
const raf = window.requestAnimationFrame;
let running = false,
lastUpdate = now(),
frame = 0;
this.start = () => {
if (running) {
return this;
}
running = true;
lastUpdate = now();
frame = raf(tick);
return this;
};
this.stop = () => {
running = false;
if (frame != 0) {
raf.cancel(frame);
}
frame = 0;
return this;
};
const tick = () => {
frame = raf(tick);
const time = now();
const dt = time - lastUpdate;
fn(dt);
lastUpdate = time;
};
}
#app canvas {
position: fixed;
top: 50%;
bottom: 0;
left: 50%;
right: 0;
width: 100vmin;
height: 70vmin;
transform: translate(-50%, -25%);
image-rendering: optimizeSpeed;
cursor: none;
margin: auto;
}
<div id="app">
</div>
GLSL does not allow you to pass functions as parameters. The snippet you linked is more of a macro, where you are supposed to manually inline the primitive.
Just above the code you copy-pasted, the definition of transform is stated:
This code bellow assumes that transform encodes only a rotation and a translation (as a 3x4 matrix for example, or as a quaternion and a vector), and that it does not contain any scaling factors in it.
To work in 2D, you use 3x3 matrices, where the upper 2x2 matrix encodes a rotation and the two first columns of the bottom row encode a translation.
Putting it all together: (replace the mainImage function of https://www.shadertoy.com/view/MldcD7 with this)
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y;
p *= 1.1;
// iFrame is a uniform that shadertoy provides: the current frame number
float angle = float(iFrame) / 60.0;
// Rotation part: rotate by `angle`, or once every 60 fps.
// Translation part: Move across the screen left to right, every 60 fps.
mat3 transform = mat3(
cos(angle), sin(angle), 0.0,
-sin(angle),cos(angle), 0.0,
(float(iFrame % 60)/60.0 - 0.5) * 2.0, 0.0, 1.0
);
vec2 tri = vec2(0.3,-1.1); // width, height
// Here, we first apply the inverse transform to our input, then pass the resulting point to our primitive, here sdTriangleIsosceles
float d = sdTriangleIsosceles( tri, (inverse(transform) * vec3(p, 1.0)).xy );
vec3 col = vec3(1.0) - sign(d)*vec3(0.1,0.4,0.7);
col *= 1.0 - exp(-2.0*abs(d));
col *= 0.8 + 0.2*cos(140.0*d);
col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.02,abs(d)) );
fragColor = vec4(col*1.2,1.0);
}
I'm working on a Three.js scene in which I'd like to update some textures after some time. I'm finding that updating the textures is very slow, however, and drags FPS to only 1-2 FPS for several seconds (when updating just a single texture).
Is there anything one can do to expedite texture updates? Any insights others can offer on this question would be very appreciated.
To see this behavior, click the window of the example below. This will load the first texture update (another click will trigger the second texture update). If you try to zoom after one of these clicks, you'll find the screen freezes and the FPS will drop terribly. Does anyone know how to fix this problem?
<html>
<head>
<style>
html, body { width: 100%; height: 100%; background: #000; }
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
</style>
</head>
<body>
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js'></script>
<script src='https://rawgit.com/YaleDHLab/pix-plot/master/assets/js/trackball-controls.js'></script>
<script src='https://rawgit.com/mrdoob/stats.js/master/build/stats.min.js'></script>
<script type='x-shader/x-vertex' id='vertex-shader'>
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec3 cameraPosition;
attribute vec3 position; // sets the blueprint's vertex positions
attribute vec3 translation; // x y translation offsets for an instance
attribute float texIdx; // the texture index to access
varying float vTexIdx;
void main() {
// set point position
vec3 pos = position + translation;
vec4 projected = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
gl_Position = projected;
// assign the varyings
vTexIdx = texIdx;
// use the delta between the point position and camera position to size point
float xDelta = pow(projected[0] - cameraPosition[0], 2.0);
float yDelta = pow(projected[1] - cameraPosition[1], 2.0);
float zDelta = pow(projected[2] - cameraPosition[2], 2.0);
float delta = pow(xDelta + yDelta + zDelta, 0.5);
gl_PointSize = 40000.0 / delta;
}
</script>
<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;
uniform sampler2D a;
uniform sampler2D b;
varying float vTexIdx;
void main() {
int textureIndex = int(vTexIdx);
vec2 uv = vec2(gl_PointCoord.x, gl_PointCoord.y);
if (textureIndex == 0) {
gl_FragColor = texture2D(a, uv);
} else if (textureIndex == 1) {
gl_FragColor = texture2D(b, uv);
}
}
</script>
<script>
/**
* Generate a scene object with a background color
**/
function getScene() {
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xaaaaaa);
return scene;
}
/**
* Generate the camera to be used in the scene
**/
function getCamera() {
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 100000);
camera.position.set(0, 1, -6000);
return camera;
}
/**
* Generate the renderer to be used in the scene
**/
function getRenderer() {
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
// Add support for retina displays
renderer.setPixelRatio(window.devicePixelRatio);
// Specify the size of the canvas
renderer.setSize(window.innerWidth, window.innerHeight);
// Add the canvas to the DOM
document.body.appendChild(renderer.domElement);
return renderer;
}
/**
* Generate the controls to be used in the scene
**/
function getControls(camera, renderer) {
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
return controls;
}
/**
* Generate the points for the scene
**/
function addPoints(scene) {
var BA = THREE.BufferAttribute;
var IBA = THREE.InstancedBufferAttribute;
var geometry = new THREE.InstancedBufferGeometry();
// add data for each observation
var n = 10000; // number of observations
var rootN = n**(1/2);
var cellSize = 20;
var translation = new Float32Array( n * 3 );
var texIdx = new Float32Array( n );
var translationIterator = 0;
var texIterator = 0;
for (var i=0; i<n*3; i++) {
var x = Math.random() * n - (n/2);
var y = Math.random() * n - (n/2);
translation[translationIterator++] = x;
translation[translationIterator++] = y;
translation[translationIterator++] = Math.random() * n - (n/2);
texIdx[texIterator++] = (x + y) > (n/8) ? 1 : 0;
}
var positionAttr = new BA(new Float32Array( [0, 0, 0] ), 3);
var translationAttr = new IBA(translation, 3, 1);
var texIdxAttr = new IBA(texIdx, 1, 1);
positionAttr.dynamic = true;
translationAttr.dynamic = true;
texIdxAttr.dynamic = true;
geometry.addAttribute('position', positionAttr);
geometry.addAttribute('translation', translationAttr);
geometry.addAttribute('texIdx', texIdxAttr);
var canvases = [
getElem('canvas', { width: 16384, height: 16384, }),
getElem('canvas', { width: 16384, height: 16384, }),
]
var textures = [
getTexture( canvases[0] ),
getTexture( canvases[1] ),
];
var material = new THREE.RawShaderMaterial({
uniforms: {
a: {
type: 't',
value: textures[0],
},
b: {
type: 't',
value: textures[1],
}
},
vertexShader: document.getElementById('vertex-shader').textContent,
fragmentShader: document.getElementById('fragment-shader').textContent,
});
var mesh = new THREE.Points(geometry, material);
mesh.frustumCulled = false; // prevent the mesh from being clipped on drag
scene.add(mesh);
// on the first window click, paint red points
// on the second window click, paint blue points
var clicks = 0;
window.addEventListener('click', function() {
if (clicks == 0 || clicks == 1) {
var canvas = canvases[clicks];
var ctx = canvas.getContext('2d');
ctx.fillStyle = clicks == 0 ? 'red' : 'blue';
ctx.rect(0, 0, 16384, 16384);
ctx.fill();
textures[clicks].needsUpdate = true;
clicks++;
}
})
}
function getTexture(canvas) {
var tex = new THREE.Texture(canvas);
tex.needsUpdate = true;
tex.flipY = false;
return tex;
}
/**
* Create an element
**/
function getElem(tag, obj) {
var obj = obj || {};
var elem = document.createElement(tag);
Object.keys(obj).forEach(function(attr) {
elem[attr] = obj[attr];
})
return elem;
}
/**
* Add stats
**/
function getStats() {
var stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '65px';
stats.domElement.style.right = '5px';
stats.domElement.style.left = 'initial';
document.body.appendChild(stats.domElement);
return stats;
}
/**
* Render!
**/
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
stats.update();
};
/**
* Main
**/
var stats = getStats();
var scene = getScene();
var camera = getCamera();
var renderer = getRenderer();
var controls = getControls(camera, renderer);
addPoints(scene);
render();
</script>
</body>
</html>
Your canvases are 16384 by 16384. That's basically insanely large.
For RGBA format, that is 1073741824 bytes.. a gigabyte of texture data that is getting sent to your GPU from the CPU when you set that texture.needsUpdate = true
You will definitely notice this getting uploaded to the card.
If your use case absolutely requires textures that large.. then you may need to consider doing incremental updates via gl.texSubImage2D, or using a bunch of smaller textures and only updating one of the per frame, or only updating those textures at the start of your app, and not thereafter.
For reference, there are very few cases i've seen where textures > 4k per side are needed.
And that is about 1/16th the size of your textures.
This has nothing to do with three.js btw. It's a fundamental characteristic of GPU/CPU interaction. Uploads and state changes are slow and have to be carefully orchestrated and monitored.