使用 webgl 在鼠标移动上创建一个 smudge/liquify 效果,该效果会连续动画回到原始状态
Creating a smudge/liquify effect on mouse move that continuously animates back to the original state using webgl
我正在尝试查找可用于创建 smudge/liquify 效果的信息或示例,该效果可以连续动画回到原始状态。
最初我正在考虑使用 three.js 或 pixi.js 渲染一些文本,然后使用鼠标事件和光线投射将网格拖出位置,我发现最接近的是这个.
https://codepen.io/shshaw/pen/qqVgbg
let renderer = PIXI.autoDetectRenderer(window.innerWidth,
window.innerHeight, { transparent: true });
我认为理想情况下,我会将文本渲染为图像,然后将涂抹效果应用于像素,然后它们会慢慢恢复到原始状态。类似这样。
http://www.duhaihang.com/#/work/
我想我可能需要使用自定义 GLSL 着色器和某种缓冲区来保存构成图像的像素的原始状态和当前状态。
如有任何帮助或指导,我们将不胜感激。
两者看起来都比较简单。
第一个,就像你提到的,你制作了一个绘制平面的顶点网格(网格)。当您四处拖动鼠标时,您将面纹理映射到平面,为鼠标接触的每个顶点添加位移。随着时间的推移将位移重置为 0(如位移量为 0)
这里有一个例子:它只是将单个顶点置换了一个随机量,而不是更可预测的量。最后,我只是节省了位移应该淡出的时间,然后在着色器中我做了一个简单的线性 lerp(可以使用更高级的 lerp 来实现反弹或其他东西)。这几乎就是着色器中发生的所有事情。
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const vs = `
attribute vec4 position;
attribute vec3 displacement;
uniform mat4 u_matrix;
uniform float u_time;
uniform float u_timeToGoBack;
varying vec2 v_texcoord;
void main() {
// because position goes -1 <-> 1 we can just use
// it for texture coords
v_texcoord = position.xy * .5 + .5;
// displacement.z is the time at which it should be undisplaced
float displaceTime = displacement.z - u_time;
float lerp = clamp(displaceTime / u_timeToGoBack, 0., 1.);
vec2 displace = displacement.xy * lerp;
gl_Position = u_matrix * (position + vec4(displace, 0, 0));
}
`;
const fs = `
precision mediump float;
uniform sampler2D texture;
varying vec2 v_texcoord;
void main() {
gl_FragColor = texture2D(texture, v_texcoord);
}
`;
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// create a grid of points in a -1 to +1 quad
const positions = [];
const displacements = [];
const indices = [];
const res = 100;
for (var y = 0; y < res; ++y) {
var v = (y / (res - 1)) * 2 - 1;
for (var x = 0; x < res; ++x) {
var u = (x / (res - 1)) * 2 - 1;
positions.push(u, v);
displacements.push(0, 0, 0);
}
}
for (var y = 0; y < res - 1; ++y) {
var off0 = (y + 0) * res;
var off1 = (y + 1) * res;
for (var x = 0; x < res - 1; ++x) {
indices.push(
off0 + x + 0, off0 + x + 1, off1 + x + 0,
off1 + x + 0, off0 + x + 1, off1 + x + 1
);
}
}
// create buffers and fills them in.
// (calls gl.createBuffer and gl.bufferData for each array)
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: { numComponents: 2, data: positions, },
displacement: { numComponents: 3, data: displacements, },
indices: indices,
});
// this will be replaced when the image has loaded;
var img = { width: 1, height: 1 };
const tex = twgl.createTexture(gl, {
src: 'https://farm6.staticflickr.com/5078/14032935559_8c13e9b181_z_d.jpg',
crossOrigin: '',
}, function(err, texture, source) {
img = source;
});
var currentTime = 0;
var currentMatrix;
const timeToGoBack = 2; // in seconds;
function render(time) {
time *= 0.001; // convert to seconds
currentTime = time;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(programInfo.program);
var aspect = img.width / img.height;
var mat = m4.ortho(0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);
mat = m4.translate(mat, [gl.canvas.clientWidth / 2, gl.canvas.clientHeight / 2, 0]);
mat = m4.scale(mat, [img.width * .25, img.height * .25, 1]);
currentMatrix = mat;
// calls gl.bindBuffer, gl.vertexAttribPointer to setup
// attributes
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: mat,
u_texture: tex,
u_time: currentTime,
u_timeToGoBack: timeToGoBack,
});
gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
const displace = new Float32Array(3);
gl.canvas.addEventListener('mousemove', function(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
const rx = event.clientX - rect.left;
const ry = event.clientY - rect.top;
const x = rx * target.width / target.clientWidth;
const y = ry * target.height / target.clientHeight;
// reverse project the mouse onto the image
var rmat = m4.inverse(currentMatrix);
var s = m4.transformPoint(
rmat, [x / target.width * 2 - 1, y / target.height * 2 - 1, 0]);
// s is now a point in the space of `position`
// lets just move closest point?
var gx = Math.round((s[0] * .5 + .5) * res);
var gy = Math.round((s[1] * .5 + .5) * res);
gx = clamp(gx, 0, res - 1);
gy = clamp(gy, 0, res - 1);
const offset = ((res - gy - 1) * res + gx) * 3 * 4;
displace[0] = rand(-.1, .1);
displace[1] = rand(-.1, .1);
displace[2] = currentTime + timeToGoBack;
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.displacement.buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, offset, displace);
});
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
对于第二个,你制作了一个置换纹理,而不是置换顶点,随着时间的推移,你将该置换重置回 0
你可以看到的例子。如果您采用该样本而不是在鼠标下绘制随机正方形,则使用该纹理作为主图像的置换。通过位移我的意思是通常你在片段着色器中查找纹理像这样
vec4 color = texture2D(someTexture, someTextureCoords);
相反,您想用位移来置换顶点坐标,像这样
// assuming the displacement texture is the same size as
// the main texture you can use the same texture coords
// first look up the displacement and convert to -1 <-> 1 range
// we're only using the R and G channels which will become U and V
// displacements to our texture coordinates
vec2 displacement = texture2D(displacementTexture, someTextureCoords).rg * 2. - 1.;
vec2 uv = someTextureCoords + displacement * displacementRange;
vec4 color = texture2d(someTexture, uv);
这是上面链接的用于置换的示例
var vs = `
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * position;
}
`;
var fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
var vsQuad = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
var fsFade = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_mixAmount;
const float kEpsilon = 2./256.;
void main() {
// convert color from 0.->1. to -1. -> +1. so we can go adjust toward zero
vec4 color = texture2D(u_texture, v_texcoord) * 2. - 1.;
// figure out how much to adjust
vec4 adjust = -color * u_mixAmount;
// If the adjustment is too small (because the texture is only 8bits)
// the adjust the minimum amount.
// Could also solve this by using floating point textures
adjust = mix(adjust, sign(color) * -kEpsilon, step(abs(adjust), vec4(kEpsilon)));
// adjust it
color += adjust;
// write it back converting back to 0 -> 1
gl_FragColor = color * .5 + .5;
}
`;
var fsDisplace = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform sampler2D u_displacementTexture;
uniform vec2 u_displacementRange;
void main() {
// assuming the displacement texture is the same size as
// the main texture you can use the same texture coords
// first look up the displacement and convert to -1 <-> 1 range
// we're only using the R and G channels which will become U and V
// displacements to our texture coordinates
vec2 displacement = texture2D(u_displacementTexture, v_texcoord).rg * 2. - 1.;
vec2 uv = v_texcoord + displacement * u_displacementRange;
gl_FragColor = texture2D(u_texture, uv);
}
`;
var $ = document.querySelector.bind(document);
var mixAmount = 0.03;
var gl = $("canvas").getContext("webgl");
var m4 = twgl.m4;
var programInfo = twgl.createProgramInfo(gl, [vs, fs]);
var fadeProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsFade]);
var displaceProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsDisplace]);
// this will be replaced when the image has loaded;
var img = { width: 1, height: 1 };
const tex = twgl.createTexture(gl, {
src: 'https://farm6.staticflickr.com/5078/14032935559_8c13e9b181_z_d.jpg',
crossOrigin: '',
flipY: true,
}, function(err, texture, source) {
img = source;
});
// Creates a -1 to +1 quad
var quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
// Creates 2 RGBA texture + depth framebuffers
var fadeAttachments = [
{ format: gl.RGBA, min: gl.NEAREST, max: gl.NEAREST, wrap: gl.CLAMP_TO_EDGE, },
{ format: gl.DEPTH_STENCIL },
];
var fadeFbi1 = twgl.createFramebufferInfo(gl, fadeAttachments);
var fadeFbi2 = twgl.createFramebufferInfo(gl, fadeAttachments);
function drawThing(gl, x, y, rotation, scale, color) {
var matrix = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
matrix = m4.translate(matrix, [x, y, 0]);
matrix = m4.rotateZ(matrix, rotation);
matrix = m4.scale(matrix, [scale, scale, 1]);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: matrix,
u_color: color,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
}
function render(time) {
if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
// set the clear color to 0.5 which is 0 displacement
// for our shader
gl.clearColor(0.5, 0.5, 0.5, 0.5);
// resize the framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi1, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi1);
gl.clear(gl.COLOR_BUFFER_BIT);
// resize the 2nd framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi2, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.clear(gl.COLOR_BUFFER_BIT);
}
// fade by copying from fadeFbi1 into fabeFbi2 using mixAmount.
// fadeFbi2 will contain mix(fadeFb1, u_fadeColor, u_mixAmount)
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.useProgram(fadeProgramInfo.program);
twgl.setBuffersAndAttributes(gl, fadeProgramInfo, quadBufferInfo);
twgl.setUniforms(fadeProgramInfo, {
u_texture: fadeFbi1.attachments[0],
u_mixAmount: mixAmount,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
// now draw new stuff to fadeFb2. Notice we don't clear!
twgl.bindFramebufferInfo(gl, fadeFbi2);
var x = rand(gl.canvas.width);
var y = rand(gl.canvas.height);
var rotation = rand(Math.PI);
var scale = rand(10, 20);
var color = [rand(1), rand(1), rand(1), 1];
drawThing(gl, x, y, rotation, scale, color);
// now use fadeFbi2 as a displacement while drawing tex to the canvas
twgl.bindFramebufferInfo(gl, null);
gl.useProgram(displaceProgramInfo.program);
twgl.setBuffersAndAttributes(gl, displaceProgramInfo, quadBufferInfo);
twgl.setUniforms(displaceProgramInfo, {
u_texture: tex,
u_displacementTexture: fadeFbi2.attachments[0],
u_displacementRange: [0.1, 0.1],
});
twgl.drawBufferInfo(gl, quadBufferInfo);
// swap the variables so we render to the opposite textures next time
var temp = fadeFbi1;
fadeFbi1 = fadeFbi2;
fadeFbi2 = temp;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
所以剩下的就是让它在鼠标下绘制而不是随机绘制
var vs = `
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * position;
}
`;
var fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
var vsQuad = `
attribute vec4 position;
attribute vec2 texcoord;
uniform mat4 u_matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = u_matrix * position;
v_texcoord = texcoord;
}
`;
var fsFade = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_mixAmount;
const float kEpsilon = 2./256.;
void main() {
vec4 color = texture2D(u_texture, v_texcoord) * 2. - 1.;
vec4 adjust = -color * u_mixAmount;
adjust = mix(adjust, sign(color) * -kEpsilon, step(abs(adjust), vec4(kEpsilon)));
color += adjust;
gl_FragColor = color * .5 + .5;
}
`;
var fsDisplace = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform sampler2D u_displacementTexture;
uniform vec2 u_displacementRange;
void main() {
// assuming the displacement texture is the same size as
// the main texture you can use the same texture coords
// first look up the displacement and convert to -1 <-> 1 range
// we're only using the R and G channels which will become U and V
// displacements to our texture coordinates
vec2 displacement = texture2D(u_displacementTexture, v_texcoord).rg * 2. - 1.;
vec2 uv = v_texcoord + displacement * u_displacementRange;
gl_FragColor = texture2D(u_texture, uv);
}
`;
var $ = document.querySelector.bind(document);
var mixAmount = 0.03;
var gl = $("canvas").getContext("webgl");
var m4 = twgl.m4;
var programInfo = twgl.createProgramInfo(gl, [vs, fs]);
var fadeProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsFade]);
var displaceProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsDisplace]);
// this will be replaced when the image has loaded;
var img = { width: 1, height: 1 };
const tex = twgl.createTexture(gl, {
src: 'https://farm6.staticflickr.com/5078/14032935559_8c13e9b181_z_d.jpg',
crossOrigin: '',
}, function(err, texture, source) {
img = source;
});
// Creates a -1 to +1 quad
var quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
// Creates 2 RGBA texture + depth framebuffers
var fadeAttachments = [
{ format: gl.RGBA,
min: gl.NEAREST,
max: gl.NEAREST,
wrap: gl.CLAMP_TO_EDGE,
},
];
var fadeFbi1 = twgl.createFramebufferInfo(gl, fadeAttachments);
var fadeFbi2 = twgl.createFramebufferInfo(gl, fadeAttachments);
function drawThing(gl, x, y, rotation, scale, color) {
var matrix = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
matrix = m4.translate(matrix, [x, y, 0]);
matrix = m4.rotateZ(matrix, rotation);
matrix = m4.scale(matrix, [scale, scale, 1]);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: matrix,
u_color: color,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
}
var drawRect = false;
var rectX;
var rectY;
var currentMatrix;
function render(time) {
if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
// set the clear color to 0.5 which is 0 displacement
// for our shader
gl.clearColor(0.5, 0.5, 0.5, 0.5);
// resize the framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi1, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi1);
gl.clear(gl.COLOR_BUFFER_BIT);
// resize the 2nd framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi2, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.clear(gl.COLOR_BUFFER_BIT);
}
// fade by copying from fadeFbi1 into fabeFbi2 using mixAmount.
// fadeFbi2 will contain mix(fadeFb1, u_fadeColor, u_mixAmount)
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.useProgram(fadeProgramInfo.program);
twgl.setBuffersAndAttributes(gl, fadeProgramInfo, quadBufferInfo);
twgl.setUniforms(fadeProgramInfo, {
u_matrix: m4.identity(),
u_texture: fadeFbi1.attachments[0],
u_mixAmount: mixAmount,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
if (drawRect) {
drawRect = false;
// now draw new stuff to fadeFb2. Notice we don't clear!
twgl.bindFramebufferInfo(gl, fadeFbi2);
var rotation = rand(Math.PI);
var scale = rand(10, 20);
var color = [rand(1), rand(1), rand(1), 1];
drawThing(gl, rectX, rectY, rotation, scale, color);
}
// now use fadeFbi2 as a displacement while drawing tex to the canvas
twgl.bindFramebufferInfo(gl, null);
var mat = m4.ortho(0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);
mat = m4.translate(mat, [gl.canvas.clientWidth / 2, gl.canvas.clientHeight / 2, 0]);
mat = m4.scale(mat, [img.width * 0.5, img.height * 0.5, 1]);
currentMatrix = mat;
gl.useProgram(displaceProgramInfo.program);
twgl.setBuffersAndAttributes(gl, displaceProgramInfo, quadBufferInfo);
twgl.setUniforms(displaceProgramInfo, {
u_matrix: mat,
u_texture: tex,
u_displacementTexture: fadeFbi2.attachments[0],
u_displacementRange: [0.05, 0.05],
});
twgl.drawBufferInfo(gl, quadBufferInfo);
// swap the variables so we render to the opposite textures next time
var temp = fadeFbi1;
fadeFbi1 = fadeFbi2;
fadeFbi2 = temp;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
gl.canvas.addEventListener('mousemove', function(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
const rx = event.clientX - rect.left;
const ry = event.clientY - rect.top;
const x = rx * target.width / target.clientWidth;
const y = ry * target.height / target.clientHeight;
// reverse project the mouse onto the image
var rmat = m4.inverse(currentMatrix);
var clipspacePoint = [x / target.width * 2 - 1, -(y / target.height * 2 - 1), 0];
var s = m4.transformPoint(rmat, clipspacePoint);
// s is now a point in the space of the image's quad. The quad goes -1 to 1
// and we're going to draw into it using pixels because drawThing takes
// a pixel value and our displacement map is the same size as the canvas
drawRect = true;
rectX = ( s[0] * .5 + .5) * gl.canvas.width;
rectY = (-s[1] * .5 + .5) * gl.canvas.height;
});
body { margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
获得第二个示例的确切效果看起来像是 运行通过某种噪声函数调整位移。您可以使用 WebGL Inspector or the Shader Editor 之类的东西来查看着色器内部并查看它们在做什么。
Here's another example 创建一个位移纹理,它比边缘更向中心位移。
注意:我应该说清楚我没有查看您链接到的示例如何工作的细节,我只是建议他们正在做一些类似的事情这个。找出他们真正在做什么的最好方法是查看他们的代码和 运行 前几段中提到的工具以查看内部情况并了解发生了什么。也许他们没有使用直接位移,而是使用法线之类的东西作为位移。也许他们不是绘制纯色(第二个和第三个示例)或纹理(第四个示例),而是使用程序生成的图案或使用基于屏幕的纹理坐标来绘制重复纹理图案。也许置换贴图是一个单独的贴图,他们有一个 "mix mask" ,它们以白色绘制并逐渐变为黑色,以决定应用多少置换贴图。在 WebGL 中有无数种方法可以做事。
我正在尝试查找可用于创建 smudge/liquify 效果的信息或示例,该效果可以连续动画回到原始状态。
最初我正在考虑使用 three.js 或 pixi.js 渲染一些文本,然后使用鼠标事件和光线投射将网格拖出位置,我发现最接近的是这个.
https://codepen.io/shshaw/pen/qqVgbg
let renderer = PIXI.autoDetectRenderer(window.innerWidth,
window.innerHeight, { transparent: true });
我认为理想情况下,我会将文本渲染为图像,然后将涂抹效果应用于像素,然后它们会慢慢恢复到原始状态。类似这样。
http://www.duhaihang.com/#/work/
我想我可能需要使用自定义 GLSL 着色器和某种缓冲区来保存构成图像的像素的原始状态和当前状态。
如有任何帮助或指导,我们将不胜感激。
两者看起来都比较简单。
第一个,就像你提到的,你制作了一个绘制平面的顶点网格(网格)。当您四处拖动鼠标时,您将面纹理映射到平面,为鼠标接触的每个顶点添加位移。随着时间的推移将位移重置为 0(如位移量为 0)
这里有一个例子:它只是将单个顶点置换了一个随机量,而不是更可预测的量。最后,我只是节省了位移应该淡出的时间,然后在着色器中我做了一个简单的线性 lerp(可以使用更高级的 lerp 来实现反弹或其他东西)。这几乎就是着色器中发生的所有事情。
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const vs = `
attribute vec4 position;
attribute vec3 displacement;
uniform mat4 u_matrix;
uniform float u_time;
uniform float u_timeToGoBack;
varying vec2 v_texcoord;
void main() {
// because position goes -1 <-> 1 we can just use
// it for texture coords
v_texcoord = position.xy * .5 + .5;
// displacement.z is the time at which it should be undisplaced
float displaceTime = displacement.z - u_time;
float lerp = clamp(displaceTime / u_timeToGoBack, 0., 1.);
vec2 displace = displacement.xy * lerp;
gl_Position = u_matrix * (position + vec4(displace, 0, 0));
}
`;
const fs = `
precision mediump float;
uniform sampler2D texture;
varying vec2 v_texcoord;
void main() {
gl_FragColor = texture2D(texture, v_texcoord);
}
`;
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// create a grid of points in a -1 to +1 quad
const positions = [];
const displacements = [];
const indices = [];
const res = 100;
for (var y = 0; y < res; ++y) {
var v = (y / (res - 1)) * 2 - 1;
for (var x = 0; x < res; ++x) {
var u = (x / (res - 1)) * 2 - 1;
positions.push(u, v);
displacements.push(0, 0, 0);
}
}
for (var y = 0; y < res - 1; ++y) {
var off0 = (y + 0) * res;
var off1 = (y + 1) * res;
for (var x = 0; x < res - 1; ++x) {
indices.push(
off0 + x + 0, off0 + x + 1, off1 + x + 0,
off1 + x + 0, off0 + x + 1, off1 + x + 1
);
}
}
// create buffers and fills them in.
// (calls gl.createBuffer and gl.bufferData for each array)
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: { numComponents: 2, data: positions, },
displacement: { numComponents: 3, data: displacements, },
indices: indices,
});
// this will be replaced when the image has loaded;
var img = { width: 1, height: 1 };
const tex = twgl.createTexture(gl, {
src: 'https://farm6.staticflickr.com/5078/14032935559_8c13e9b181_z_d.jpg',
crossOrigin: '',
}, function(err, texture, source) {
img = source;
});
var currentTime = 0;
var currentMatrix;
const timeToGoBack = 2; // in seconds;
function render(time) {
time *= 0.001; // convert to seconds
currentTime = time;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(programInfo.program);
var aspect = img.width / img.height;
var mat = m4.ortho(0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);
mat = m4.translate(mat, [gl.canvas.clientWidth / 2, gl.canvas.clientHeight / 2, 0]);
mat = m4.scale(mat, [img.width * .25, img.height * .25, 1]);
currentMatrix = mat;
// calls gl.bindBuffer, gl.vertexAttribPointer to setup
// attributes
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: mat,
u_texture: tex,
u_time: currentTime,
u_timeToGoBack: timeToGoBack,
});
gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
const displace = new Float32Array(3);
gl.canvas.addEventListener('mousemove', function(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
const rx = event.clientX - rect.left;
const ry = event.clientY - rect.top;
const x = rx * target.width / target.clientWidth;
const y = ry * target.height / target.clientHeight;
// reverse project the mouse onto the image
var rmat = m4.inverse(currentMatrix);
var s = m4.transformPoint(
rmat, [x / target.width * 2 - 1, y / target.height * 2 - 1, 0]);
// s is now a point in the space of `position`
// lets just move closest point?
var gx = Math.round((s[0] * .5 + .5) * res);
var gy = Math.round((s[1] * .5 + .5) * res);
gx = clamp(gx, 0, res - 1);
gy = clamp(gy, 0, res - 1);
const offset = ((res - gy - 1) * res + gx) * 3 * 4;
displace[0] = rand(-.1, .1);
displace[1] = rand(-.1, .1);
displace[2] = currentTime + timeToGoBack;
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.displacement.buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, offset, displace);
});
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
对于第二个,你制作了一个置换纹理,而不是置换顶点,随着时间的推移,你将该置换重置回 0
你可以看到
vec4 color = texture2D(someTexture, someTextureCoords);
相反,您想用位移来置换顶点坐标,像这样
// assuming the displacement texture is the same size as
// the main texture you can use the same texture coords
// first look up the displacement and convert to -1 <-> 1 range
// we're only using the R and G channels which will become U and V
// displacements to our texture coordinates
vec2 displacement = texture2D(displacementTexture, someTextureCoords).rg * 2. - 1.;
vec2 uv = someTextureCoords + displacement * displacementRange;
vec4 color = texture2d(someTexture, uv);
这是上面链接的用于置换的示例
var vs = `
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * position;
}
`;
var fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
var vsQuad = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
var fsFade = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_mixAmount;
const float kEpsilon = 2./256.;
void main() {
// convert color from 0.->1. to -1. -> +1. so we can go adjust toward zero
vec4 color = texture2D(u_texture, v_texcoord) * 2. - 1.;
// figure out how much to adjust
vec4 adjust = -color * u_mixAmount;
// If the adjustment is too small (because the texture is only 8bits)
// the adjust the minimum amount.
// Could also solve this by using floating point textures
adjust = mix(adjust, sign(color) * -kEpsilon, step(abs(adjust), vec4(kEpsilon)));
// adjust it
color += adjust;
// write it back converting back to 0 -> 1
gl_FragColor = color * .5 + .5;
}
`;
var fsDisplace = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform sampler2D u_displacementTexture;
uniform vec2 u_displacementRange;
void main() {
// assuming the displacement texture is the same size as
// the main texture you can use the same texture coords
// first look up the displacement and convert to -1 <-> 1 range
// we're only using the R and G channels which will become U and V
// displacements to our texture coordinates
vec2 displacement = texture2D(u_displacementTexture, v_texcoord).rg * 2. - 1.;
vec2 uv = v_texcoord + displacement * u_displacementRange;
gl_FragColor = texture2D(u_texture, uv);
}
`;
var $ = document.querySelector.bind(document);
var mixAmount = 0.03;
var gl = $("canvas").getContext("webgl");
var m4 = twgl.m4;
var programInfo = twgl.createProgramInfo(gl, [vs, fs]);
var fadeProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsFade]);
var displaceProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsDisplace]);
// this will be replaced when the image has loaded;
var img = { width: 1, height: 1 };
const tex = twgl.createTexture(gl, {
src: 'https://farm6.staticflickr.com/5078/14032935559_8c13e9b181_z_d.jpg',
crossOrigin: '',
flipY: true,
}, function(err, texture, source) {
img = source;
});
// Creates a -1 to +1 quad
var quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
// Creates 2 RGBA texture + depth framebuffers
var fadeAttachments = [
{ format: gl.RGBA, min: gl.NEAREST, max: gl.NEAREST, wrap: gl.CLAMP_TO_EDGE, },
{ format: gl.DEPTH_STENCIL },
];
var fadeFbi1 = twgl.createFramebufferInfo(gl, fadeAttachments);
var fadeFbi2 = twgl.createFramebufferInfo(gl, fadeAttachments);
function drawThing(gl, x, y, rotation, scale, color) {
var matrix = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
matrix = m4.translate(matrix, [x, y, 0]);
matrix = m4.rotateZ(matrix, rotation);
matrix = m4.scale(matrix, [scale, scale, 1]);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: matrix,
u_color: color,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
}
function render(time) {
if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
// set the clear color to 0.5 which is 0 displacement
// for our shader
gl.clearColor(0.5, 0.5, 0.5, 0.5);
// resize the framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi1, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi1);
gl.clear(gl.COLOR_BUFFER_BIT);
// resize the 2nd framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi2, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.clear(gl.COLOR_BUFFER_BIT);
}
// fade by copying from fadeFbi1 into fabeFbi2 using mixAmount.
// fadeFbi2 will contain mix(fadeFb1, u_fadeColor, u_mixAmount)
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.useProgram(fadeProgramInfo.program);
twgl.setBuffersAndAttributes(gl, fadeProgramInfo, quadBufferInfo);
twgl.setUniforms(fadeProgramInfo, {
u_texture: fadeFbi1.attachments[0],
u_mixAmount: mixAmount,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
// now draw new stuff to fadeFb2. Notice we don't clear!
twgl.bindFramebufferInfo(gl, fadeFbi2);
var x = rand(gl.canvas.width);
var y = rand(gl.canvas.height);
var rotation = rand(Math.PI);
var scale = rand(10, 20);
var color = [rand(1), rand(1), rand(1), 1];
drawThing(gl, x, y, rotation, scale, color);
// now use fadeFbi2 as a displacement while drawing tex to the canvas
twgl.bindFramebufferInfo(gl, null);
gl.useProgram(displaceProgramInfo.program);
twgl.setBuffersAndAttributes(gl, displaceProgramInfo, quadBufferInfo);
twgl.setUniforms(displaceProgramInfo, {
u_texture: tex,
u_displacementTexture: fadeFbi2.attachments[0],
u_displacementRange: [0.1, 0.1],
});
twgl.drawBufferInfo(gl, quadBufferInfo);
// swap the variables so we render to the opposite textures next time
var temp = fadeFbi1;
fadeFbi1 = fadeFbi2;
fadeFbi2 = temp;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
所以剩下的就是让它在鼠标下绘制而不是随机绘制
var vs = `
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * position;
}
`;
var fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
var vsQuad = `
attribute vec4 position;
attribute vec2 texcoord;
uniform mat4 u_matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = u_matrix * position;
v_texcoord = texcoord;
}
`;
var fsFade = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_mixAmount;
const float kEpsilon = 2./256.;
void main() {
vec4 color = texture2D(u_texture, v_texcoord) * 2. - 1.;
vec4 adjust = -color * u_mixAmount;
adjust = mix(adjust, sign(color) * -kEpsilon, step(abs(adjust), vec4(kEpsilon)));
color += adjust;
gl_FragColor = color * .5 + .5;
}
`;
var fsDisplace = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform sampler2D u_displacementTexture;
uniform vec2 u_displacementRange;
void main() {
// assuming the displacement texture is the same size as
// the main texture you can use the same texture coords
// first look up the displacement and convert to -1 <-> 1 range
// we're only using the R and G channels which will become U and V
// displacements to our texture coordinates
vec2 displacement = texture2D(u_displacementTexture, v_texcoord).rg * 2. - 1.;
vec2 uv = v_texcoord + displacement * u_displacementRange;
gl_FragColor = texture2D(u_texture, uv);
}
`;
var $ = document.querySelector.bind(document);
var mixAmount = 0.03;
var gl = $("canvas").getContext("webgl");
var m4 = twgl.m4;
var programInfo = twgl.createProgramInfo(gl, [vs, fs]);
var fadeProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsFade]);
var displaceProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsDisplace]);
// this will be replaced when the image has loaded;
var img = { width: 1, height: 1 };
const tex = twgl.createTexture(gl, {
src: 'https://farm6.staticflickr.com/5078/14032935559_8c13e9b181_z_d.jpg',
crossOrigin: '',
}, function(err, texture, source) {
img = source;
});
// Creates a -1 to +1 quad
var quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
// Creates 2 RGBA texture + depth framebuffers
var fadeAttachments = [
{ format: gl.RGBA,
min: gl.NEAREST,
max: gl.NEAREST,
wrap: gl.CLAMP_TO_EDGE,
},
];
var fadeFbi1 = twgl.createFramebufferInfo(gl, fadeAttachments);
var fadeFbi2 = twgl.createFramebufferInfo(gl, fadeAttachments);
function drawThing(gl, x, y, rotation, scale, color) {
var matrix = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
matrix = m4.translate(matrix, [x, y, 0]);
matrix = m4.rotateZ(matrix, rotation);
matrix = m4.scale(matrix, [scale, scale, 1]);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: matrix,
u_color: color,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
}
var drawRect = false;
var rectX;
var rectY;
var currentMatrix;
function render(time) {
if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
// set the clear color to 0.5 which is 0 displacement
// for our shader
gl.clearColor(0.5, 0.5, 0.5, 0.5);
// resize the framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi1, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi1);
gl.clear(gl.COLOR_BUFFER_BIT);
// resize the 2nd framebuffer's attachments so their the
// same size as the canvas
twgl.resizeFramebufferInfo(gl, fadeFbi2, fadeAttachments);
// clear the color buffer to 0.5
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.clear(gl.COLOR_BUFFER_BIT);
}
// fade by copying from fadeFbi1 into fabeFbi2 using mixAmount.
// fadeFbi2 will contain mix(fadeFb1, u_fadeColor, u_mixAmount)
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.useProgram(fadeProgramInfo.program);
twgl.setBuffersAndAttributes(gl, fadeProgramInfo, quadBufferInfo);
twgl.setUniforms(fadeProgramInfo, {
u_matrix: m4.identity(),
u_texture: fadeFbi1.attachments[0],
u_mixAmount: mixAmount,
});
twgl.drawBufferInfo(gl, quadBufferInfo);
if (drawRect) {
drawRect = false;
// now draw new stuff to fadeFb2. Notice we don't clear!
twgl.bindFramebufferInfo(gl, fadeFbi2);
var rotation = rand(Math.PI);
var scale = rand(10, 20);
var color = [rand(1), rand(1), rand(1), 1];
drawThing(gl, rectX, rectY, rotation, scale, color);
}
// now use fadeFbi2 as a displacement while drawing tex to the canvas
twgl.bindFramebufferInfo(gl, null);
var mat = m4.ortho(0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);
mat = m4.translate(mat, [gl.canvas.clientWidth / 2, gl.canvas.clientHeight / 2, 0]);
mat = m4.scale(mat, [img.width * 0.5, img.height * 0.5, 1]);
currentMatrix = mat;
gl.useProgram(displaceProgramInfo.program);
twgl.setBuffersAndAttributes(gl, displaceProgramInfo, quadBufferInfo);
twgl.setUniforms(displaceProgramInfo, {
u_matrix: mat,
u_texture: tex,
u_displacementTexture: fadeFbi2.attachments[0],
u_displacementRange: [0.05, 0.05],
});
twgl.drawBufferInfo(gl, quadBufferInfo);
// swap the variables so we render to the opposite textures next time
var temp = fadeFbi1;
fadeFbi1 = fadeFbi2;
fadeFbi2 = temp;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
gl.canvas.addEventListener('mousemove', function(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
const rx = event.clientX - rect.left;
const ry = event.clientY - rect.top;
const x = rx * target.width / target.clientWidth;
const y = ry * target.height / target.clientHeight;
// reverse project the mouse onto the image
var rmat = m4.inverse(currentMatrix);
var clipspacePoint = [x / target.width * 2 - 1, -(y / target.height * 2 - 1), 0];
var s = m4.transformPoint(rmat, clipspacePoint);
// s is now a point in the space of the image's quad. The quad goes -1 to 1
// and we're going to draw into it using pixels because drawThing takes
// a pixel value and our displacement map is the same size as the canvas
drawRect = true;
rectX = ( s[0] * .5 + .5) * gl.canvas.width;
rectY = (-s[1] * .5 + .5) * gl.canvas.height;
});
body { margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
获得第二个示例的确切效果看起来像是 运行通过某种噪声函数调整位移。您可以使用 WebGL Inspector or the Shader Editor 之类的东西来查看着色器内部并查看它们在做什么。
Here's another example 创建一个位移纹理,它比边缘更向中心位移。
注意:我应该说清楚我没有查看您链接到的示例如何工作的细节,我只是建议他们正在做一些类似的事情这个。找出他们真正在做什么的最好方法是查看他们的代码和 运行 前几段中提到的工具以查看内部情况并了解发生了什么。也许他们没有使用直接位移,而是使用法线之类的东西作为位移。也许他们不是绘制纯色(第二个和第三个示例)或纹理(第四个示例),而是使用程序生成的图案或使用基于屏幕的纹理坐标来绘制重复纹理图案。也许置换贴图是一个单独的贴图,他们有一个 "mix mask" ,它们以白色绘制并逐渐变为黑色,以决定应用多少置换贴图。在 WebGL 中有无数种方法可以做事。