学习GLSL的基础知识是小菜一碟。然而,使用这些知识来创造效果可能会令人生畏,因为您可能会感到迷失,不确定从哪里开始。如果这听起来很熟悉,那么本教程就是为你准备的。
分辨率、鼠标和时间
在第一个例子中,我们将使用Shader的编辑器。打开它,让我们开始我们的第一个例子。
首先,让我们讨论u_resolution
,一个包含画布宽度和高度vec2
统一变量。
众所周知,GLSL在0到1的坐标系中运行:
为了实现这一点,我们将当前片段的坐标(gl_FragCoord
)除以分辨率(u_resolution
)。
GLSL
vec2 st = gl_FragCoord.xy / u_resolution.xy;
将产生一个水平渐变,其中0表示黑色,1表示白色。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
vec3 color = vec3(st.x);
gl_FragColor = vec4(color, 1.);
}
将st.x
替换为st.y
以创建垂直渐变。
GLSL
vec3 color = vec3(st.y);
要反转颜色,只需从1中减去该值。
GLSL
vec3 color = vec3(1. - st.y);
下一个统一变量是光标的坐标(u_mouse
),也用像素表示。我们通过将它们除以分辨率来将其标准化。
GLSL
vec2 mousePos = u_mouse.xy / u_resolution.xy;
允许我们使用鼠标位置的x坐标来调整梯度。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
vec2 mousePos = u_mouse.xy / u_resolution.xy;
vec3 color = vec3(mousePos.x);
gl_FragColor = vec4(color, 1.);
}
现在让我们让事情变得更有趣一点。我们将创建一个围绕光标的渐变圆圈。
为了实现这一点,我们将根据片段与鼠标位置的距离设置片段的颜色。距离越近,片段越暗,值接近0(黑色)。
为了计算距离,我们将使用预定义的distance()
函数。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec2 mousePos = u_mouse.xy / u_resolution.xy;
float d = distance(st, mousePos);
vec3 color = vec3(d);
gl_FragColor = vec4(color, 1.);
}
反转颜色:
GLSL
float d = 1. - distance(st, mousePos);
为了控制圆的半径,我们将使用pow()
函数。增加指数将减小圆的大小。
GLSL
vec3 color = vec3(pow(d, 10.));
第三个统一变量是一个代表时间的浮点数。它是着色器开始运行后的秒数。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec2 mousePos = u_mouse.xy / u_resolution.xy;
float d = 1. - distance(st, mousePos);
vec3 color = vec3(pow(d, 10. * u_time));
gl_FragColor = vec4(color, 1.);
}
这会导致圆消失,接下来,我们将对时间应用sin()
函数,该函数返回-1和1之间的值,使圆具有动画效果。
GLSL
vec3 color = vec3(pow(d, 10. * sin(u_time)));
请注意,当sin()
返回负值时,画布将在较长时间内保持白色。
为了平衡,我们将使用abs()
函数来只考虑正值。
GLSL
vec3 color = vec3(pow(d, 10. * abs(sin(u_time))));
GLSL
vec3 color = vec3(pow(d, 10. * abs(1.2 - sin(u_time))));
圆与环
这里的主要重点是避免值修改,这会导致创建渐变。
例如,如果我想要一个定义好的黑色圆,我需要它的半径内的值正好为0,其余的设置为1,避免任何中间值。
为了实现这一点,我们可以使用GLSL内置函数step()
。这个函数比较它的两个输入:如果第二个参数小于第一个,它返回0;如果它大于,它返回1。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float d1 = step(0.5, distance(vec2(0.5), st));
vec3 color = vec3(d1);
gl_FragColor = vec4(color, 1.);
}
如果当前片段到中心的距离小于0.5,则将其设置为黑色;否则,将其设置为白色。
GLSL
float d1 = step(0.5, distance(vec2(0.5), st));
现在,要绘制圆环,我将创建另一个圆。
GLSL
float d1 = step(0.44, distance(vec2(0.5), st));
float d2 = step(0.6, 1. - distance(vec2(0.5), st));
vec3 color = vec3(d1 + d2);
现在,用于计算d1
的step()
的参数表示圆环的外半径,而用于计算d2
的step()
的第一个参数表示圆环的内半径。
脉动光
在GLSL中,内置函数fact()
返回浮点数的小数部分。
所以,当我在这里应用fact()
时,你可以看到我们获得了几乎相同的梯度。如果不是完全相同的话,可以使用st.x
时。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 color = vec3(fract(st.x));
gl_FragColor = vec4(color, 1.);
}
现在,将参数乘以10。你会得到一个重复的渐变图案。
GLSL
vec3 color = vec3(fract(10. * st.x));
现在,要将其转换为放射状图案,请将st.x
替换为与中心的距离。
GLSL
float d = distance(vec2(0.5), st);
vec3 color = vec3(fract(10. * d));
若要设置动画,请减去u_time
。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float d = distance(vec2(0.5), st);
vec3 color = vec3(fract(10. * d - u_time));
gl_FragColor = vec4(color, 1.);
}
此外,将所有内容除以距离,然后再除以40。
GLSL
vec3 color = vec3(fract(10. * d - u_time)) / d / 40.;
最后,让我们介绍一些颜色。为此,创建一个包含任意值的vec3
变量。然后,您可以尝试使用各种运算符来实现不同的视觉效果。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float d = distance(vec2(0.5), st);
vec3 color = vec3(fract(10. * d - u_time)) / d / 40.;
vec3 color2 = vec3(0.1, 0.5, 0.9);
// Try /, +, and *
gl_FragColor = vec4(color - color2, 1.);
}
国际象棋
floor()
作用是:将浮点数向下舍入为小于或等于原始数字的最近整数。
示例:
GLSL
floor(0.7) === 0.
floor(2.2) === 2.
floor(3.9) === 3.
另一方面,mod()
计算两个数之间除法运算的余数。
示例:
GLSL
mod(6., 2.) === 0.
mod(5., 2.) === 1.
mod(10., 4.) === 2.
现在,当我们对st.x
应用floor()
函数时,我们仍然得到相同的黑色画布。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float checkerX = floor(st.x);
vec3 checker = vec3(checkerX);
gl_FragColor = vec4(checker, 1.);
}
要进行明显的更改,请将输入乘以任意数字。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float frequency = 5.;
float checkerX = floor(st.x * frequency);
vec3 checker = vec3(checkerX);
gl_FragColor = vec4(checker, 1.);
}
完成后,将mod()
函数应用于结果。
GLSL
vec3 checker = vec3(mod(checkerX, 2.0));
接下来,使用st.y
而不是st.x
来创建水平线。然后,将它们添加到mod()
以形成棋盘模式。
GLSL
float checkerX = floor(st.x * frequency);
float checkerY = floor(st.y * frequency);
vec3 checker = vec3(mod(checkerX + checkerY, 2.));
GLSl
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float frequency = 5.;
float checkerX = floor(st.x * frequency + u_time);
float checkerY = floor(st.y * frequency);
vec3 checker = vec3(mod(checkerX + checkerY, 2.));
gl_FragColor = vec4(checker, 1.);
}
为了控制速度,我们需要将u_time
乘以另一个浮点数。
GLSL
float speed = 2.;
float checkerX = floor(st.x * frequency + u_time * speed);
旋转和缩放
为了旋转图形,我们需要使用旋转矩阵。
因此,我将创建一个函数。它以旋转角度为参数,并返回一个旋转矩阵。
GLSL
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
现在,当我们将此应用于画布时,您会注意到旋转原点位于(0,0)点。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st *= rotate(sin(u_time * 0.1) * 5.);
float frequency = 5.;
float checkerX = floor(st.x * frequency);
float checkerY = floor(st.y * frequency);
vec3 checker = vec3(mod(checkerX + checkerY, 2.));
vec3 color = vec3(0.423, 0.435, 0.800);
gl_FragColor = vec4(checker + color, 1.);
}
若要使画布的中心成为旋转原点。请在应用旋转之前将坐标系移动一半,然后再将其移回,以调整坐标系。
GLSl
st -= vec2(0.5);
st *= rotate(sin(u_time * 0.1) * 5.);
st += vec2(0.5);
同样的原则也适用于缩放对象;我们只需要使用不同的矩阵进行缩放。
GLSL
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
mat2 scale(vec2 scale) {
return mat2(scale.x, 0., 0., scale.y);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st -= vec2(0.5);
st *= rotate(sin(u_time * 0.1) * 5.);
st += vec2(0.5);
st -= vec2(0.5);
st *= scale(vec2(sin(u_time * 0.1) * 8.));
st += vec2(0.5);
float frequency = 5.;
float checkerX = floor(st.x * frequency);
float checkerY = floor(st.y * frequency);
vec3 checker = vec3(mod(checkerX + checkerY, 2.));
vec3 color = vec3(0.423, 0.435, 0.800);
gl_FragColor = vec4(checker + color, 1.);
}
将Shader集成到Three.js应用程序中
Book of Shader's Editor中的正方形表示整个场景。但是,在实际示例中,您可能希望将相同的效果应用于场景中的平面,而不是整个场景本身。
首先,创建一个Three.js应用程序或使用我的Three.js样板。
接下来,将以下代码复制并粘贴到main.js和index.html文件中。
main.js:
js
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// Sets orbit control to move the camera around
const orbit = new OrbitControls(camera, renderer.domElement);
// Camera positioning
camera.position.set(6, 8, 14);
orbit.update();
const uniforms = {
u_time: { value: 0.0 },
u_resolution: {
value: new THREE.Vector2(
window.innerWidth,
window.innerHeight
).multiplyScalar(window.devicePixelRatio),
},
u_mouse: { value: new THREE.Vector2(0.0, 0.0) },
};
window.addEventListener('mousemove', function (e) {
uniforms.u_mouse.value.set(
e.offsetX / this.window.innerWidth,
1 - eoffsetnY / this.window.innerHeight
);
});
const geometry = new THREE.PlaneGeometry(10, 10, 30, 30);
const customMaterial = new THREE.ShaderMaterial({
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
uniforms,
});
const mesh = new THREE.Mesh(geometry, customMaterial);
scene.add(mesh);
const clock = new THREE.Clock();
function animate() {
uniforms.u_time.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
window.addEventListener('resize', function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
index.html:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wael Yasmina Three.js boilerplate</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<script id="vertexshader" type="vertex">
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script id="fragmentshader" type="fragment">
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
void main() {
gl_FragColor = vec4(1.0);
}
</script>
<script src="/main.js" type="module"></script>
</body>
</html>
现在,将第一个示例中的着色器复制并粘贴到应用程序的片段着色器中。
js
<script id="fragmentshader" type="fragment">
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float d = 1. - distance(st, u_mouse);
vec3 color = vec3(pow(d, 10. * abs(sin(u_time))));
gl_FragColor = vec4(color, 1.);
}
</script>
现在,您应该看到相同的动画,但偏移量较大。这是因为效果是基于整个场景的大小而不仅仅是平面的大小来应用的。
要解决这个问题,首先将鼠标位置的x坐标乘以窗口的宽高比。
js
window.addEventListener('mousemove', function (e) {
const vpRatio = this.window.innerWidth / this.window.innerHeight;
uniforms.u_mouse.value.set(
(e.offsetX / this.window.innerWidth) * vpRatio,
1 - e.offsetY / this.window.innerHeight
);
});
下来,在片段着色器中,将画布的x坐标乘以其纵横比。
GLSL
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st.x *= u_resolution.x / u_resolution.y;
float d = 1. - distance(st, u_mouse);
vec3 color = vec3(pow(d, 10. * abs(sin(u_time))));
gl_FragColor = vec4(color, 1.);
}
纹理
在这个例子中,我们将看到如何将纹理映射到圆柱体上。
首先,将图像放置在项目目录的公用文件夹中。
接下来,在main.js文件中,加载纹理并将其传递给uniforms
对象。
js
const uniforms = {
u_texture: { value: new THREE.TextureLoader().load('/fries.jpg') },
};
js
//const geometry = new THREE.PlaneGeometry(10, 10, 30, 30);
const geometry = new THREE.CylinderGeometry(2, 2, 0, 100);
const customMaterial = new THREE.ShaderMaterial({
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
uniforms,
});
const mesh = new THREE.Mesh(geometry, customMaterial);
scene.add(mesh);
现在,我们将使用texture2D()
函数,它有两个参数:图像和纹理坐标。
该函数的目的是从纹理中检索颜色信息,也称为纹理元素。然后,在指定的纹理坐标处对该2D纹理进行采样。
您可以观察到圆柱体现在显示了纹理的一部分,从而创建了一个很酷的效果。但是,如果你想让纹理适合物体的表面,你需要将圆柱体的纹理坐标传递给texture2D()
函数,而不是整个场景的坐标。
为了实现这一点,我们需要创建一个可变的变量来将网格的纹理坐标从顶点着色器传输到片段着色器。然后,将预定义uv
变量的值赋给它。
js
<script id="vertexshader" type="vertex">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
js
<script id="fragmentshader" type="fragment">
varying vec2 vUv;
uniform sampler2D u_texture;
void main() {
vec4 texel = texture2D(u_texture, vUv);
gl_FragColor = texel;
}
</script>
雷达信号
首先,我将演示如何使用此图像创建类似雷达的效果。
首先,我将选择一个任意的颜色,并使用纹理的纹理元素的阿尔法通道。另外,不要忘记加载纹理并将其与sampler2D
变量相关联。
GLSL
varying vec2 vUv;
uniform sampler2D u_texture;
void main() {
// u_texture is the black and white image
vec4 texel = texture2D(u_texture, vUv);
gl_FragColor = vec4(vec3(0.4, 0.5, 1.0), texel.r);
}
正如你所看到的,我们得到了一个放射状的蓝色渐变,但是透明度不起作用。
若要解决此问题,请将材质的透明
属性设置为true
。
GLSL
varying vec2 vUv;
uniform float u_time;
uniform sampler2D u_texture;
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
void main() {
vec2 vUv = vUv;
vUv -= vec2(0.5);
vUv *= rotate(sin(u_time * 0.1) * 5.);
vUv += vec2(0.5);
// u_texture is the black and white image
vec4 texel = texture2D(u_texture, vUv);
gl_FragColor = vec4(vec3(0.4, 0.5, 1.0), texel.r);
}
我们可以做的另一件有趣的事情是在另一个纹理上叠加这个效果。
为了实现这一点,我将首先从纹理传递RGB值。
GLSL
varying vec2 vUv;
uniform float u_time;
uniform sampler2D u_texture;
uniform sampler2D u_texture2;
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
void main() {
vec2 vUv = vUv;
vUv -= vec2(0.5);
vUv *= rotate(sin(u_time * 0.1) * 5.);
vUv += vec2(0.5);
// u_texture is the black and white image
vec4 texel = texture2D(u_texture, vUv);
// The cupcake image
vec4 texel2 = texture2D(u_texture2, vUv);
gl_FragColor = vec4(texel2.rgb, texel.r);
}
为了防止第二个纹理旋转,我将创建一个变量来保存旋转之前的原始UV坐标,并将其传递给texture2D()
函数。
GLSL
varying vec2 vUv;
uniform float u_time;
uniform sampler2D u_texture;
uniform sampler2D u_texture2;
mat2 rotate(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
void main() {
vec2 vUv = vUv;
vec2 vUv2 = vUv;
vUv -= vec2(0.5);
vUv *= rotate(sin(u_time * 0.1) * 5.);
vUv += vec2(0.5);
// u_texture is the black and white image
vec4 texel = texture2D(u_texture, vUv);
// The cupcake image
vec4 texel2 = texture2D(u_texture2, vUv2);
gl_FragColor = vec4(texel2.rgb, texel.r);
}
淡入
clamp()
函数接受三个输入:数值,最小值和最大值定义的范围。如果第一个输入下降值在该范围内,则函数返回相同的值。如果它小于最小值,则函数返回范围的最小值。相反,如果它大于最大值,则返回范围的最大值。
mix()
函数在两个值之间执行线性插值,也称为lerp。简单地说,它返回在这两个值之间转换的值。
也就是说,我们将使用这两个函数在网格上纹理之间创建平滑过渡效果。
首先,准备好两个图像之间的转换。
现在,在uniforms
对象中创建一个名为mixRatio的
新float属性。此变量将控制过渡效果的量。
接下来,安装lil-gui,这样我们就可以通过接口控制该变量的值。
导入库,创建params
对象,并将params
对象中mixRatio
属性的值链接到uniforms
对象中的mixRatio
。
js
import { GUI } from 'lil-gui';
const gui = new GUI();
const params = {
mixRatio: 0.0,
};
gui.add(params, 'mixRatio', 0.0, 1.0).onChange(function (value) {
uniforms.mixRatio.value = value;
});
const uniforms = {
u_texture: { value: new THREE.TextureLoader().load('/burger1.jpg') },
u_texture2: { value: new THREE.TextureLoader().load('/burger2.jpg') },
u_transition: { value: new THREE.TextureLoader().load('/transition.png') },
u_transition2: { value: new THREE.TextureLoader().load('/transition2.png') },
mixRatio: { value: 0.0 },
};
这是片段着色器。
GLSL
varying vec2 vUv;
uniform float u_time;
uniform sampler2D u_texture;
uniform sampler2D u_texture2;
uniform sampler2D u_transition;
uniform sampler2D u_transition2;
uniform float mixRatio;
void main() {
vec2 vUv = vUv;
// burger 1 image
vec4 texel = texture2D(u_texture, vUv);
// Burger2 image
vec4 texel2 = texture2D(u_texture2, vUv);
gl_FragColor = mix(texel, texel2, mixRatio);
}
首先,让我们通过调整mixRatio
值来观察对两个纹理的mix()
函数的基本调用。
通过这样做,您将看到当mixRatio
为0时,只有第一个纹理可见。当它为1时,只有第二个纹理可见
现在,我们将使用另一个纹理来塑造过渡效果。为此,我们将使用一些数学方法,包括前面讨论的clamp()
函数,来控制混合的值。
GLSL
varying vec2 vUv;
uniform sampler2D u_texture;
uniform sampler2D u_texture2;
uniform sampler2D u_transition;
uniform sampler2D u_transition2;
uniform float mixRatio;
void main() {
vec2 vUv = vUv;
// burger 1 image
vec4 texel = texture2D(u_texture, vUv);
// Burger 2 image
vec4 texel2 = texture2D(u_texture2, vUv);
// transition texture 1
vec4 transitionTexel = texture2D(u_transition, vUv);
// transition texture 2
vec4 transitionTexel2 = texture2D(u_transition2, vUv);
float r = mixRatio * 1.6 - 0.3;
// Try transitionTexel2 for another effect
float mixF = clamp((transitionTexel.r - r) * 3.33, 0.0, 1.0);
gl_FragColor = mix(texel, texel2, mixF);
}