前言
上一篇文章「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」里古柳带大家用 Three.js Shader 粒子系统实现了这个非常漂亮的星系效果。文章在上周三发布后上了掘金热榜,并且截至目前点赞数、收藏数双双破百,成为本 Shader 系列教程里阅读量最高的一篇(马上破万)。果然大家更喜欢看这种实现完整、实际、酷炫效果的文章,这倒是和我设想的一样。
正好上篇讲到粒子系统,我想不妨继续趁热打铁讲解下这个带我入坑 Shader 的效果 Pepyaka
,想来这应该也是很多前端、程序员梦寐以求想要实现的酷炫效果吧。下图是本文最终实现的效果,GIF 不够清晰,大家可以去 Codepen 查看源码和效果,代码后续也会同步到 GitHub。
Pepyaka
这个效果本来出自于 Grant Yi 的个人网站,其实我一直不知道这个词到底啥意思,但就当成该效果的代名词这么叫着。可惜原网站换成了这个效果,虽然仍是 Shader 实现、同样酷炫,但看不到 Pepyaka
还是可惜。
在「断更19个月,携 Three.js Shader 归来!(上)- 牛衣古柳 - 20230416」一文里我提过,当初入门 Three.js 后因为对粒子系统感兴趣,于是在油管搜教程,然后看到 Yuri Artiukh
复现 Pepyaka
的教程——「#s3e6 ALL YOUR HTML, Making Pepyaka with Three.js - 20191201」——在视频里残存的片段里窥见到 Pepyaka
如此丝滑、酷炫、漂亮的效果,于是入坑 Shader,再然后有了现在输出 Shader 教程这桩事。
时隔两年古柳终于可以在原视频的基础上融入自己会的一些 Shader 效果,更近一步实现出更贴近原作的各种效果,并通过文章教给大家,让大家也能上手实现这样酷炫的效果。
中心球体
闲言少叙,进入正题。让我们同样从线框模式下的白色球体开始讲起。
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
let w = window.innerWidth;
let h = window.innerHeight;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, w / h, 0.01, 1000);
camera.position.set(0, 0, 4);
camera.lookAt(new THREE.Vector3());
const renderer = new THREE.WebGLRenderer({
antialias: true,
// alpha: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(w, h);
renderer.setClearColor(0x0a0a0f, 1);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const vertexShader = /* GLSL */ `
uniform float uTime;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = /* GLSL */ `
void main() {
gl_FragColor = vec4(vec3(1.0), 1.0);
}
`;
const sphereMaterial = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
},
// wireframe: true,
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);
const clock = new THREE.Clock();
function render() {
const time = clock.getElapsedTime();
sphereMaterial.uniforms.uTime.value = time;
sphere.rotation.y = time;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
顶点偏移
接着我们用 noise 噪声函数对顶点位置进行偏移从而改变球体形状、并用 noise 值控制 HSV 颜色模式里的 Hue 色相值,这些内容在「手把手带你入门 Three.js Shader 系列(六) - 牛衣古柳 - 20231220」、「手把手带你入门 Three.js Shader 系列(七) - 牛衣古柳 - 20240206」两篇文章里已经详细地讲过,大家可以去自行学习。
这里简单过一遍,我们谷歌搜索 GLSL noise function
,从这里拷贝 Simplex 4D Noise
函数,接着对每个顶点产生一个 noise 数值(不同 noise 函数返回的值范围可能是0-1,也可能是-1-1,大家可以用第七篇里讲的方法去查看,这里不太重要也就不带大家看了),将该数值乘上法线 normal 作为在该偏移方向上偏移的程度,再加上原始 position 就是偏移后的顶点坐标。
// vertex shader
uniform float uTime;
// Simplex 4D Noise
// by Ian McEwan, Ashima Arts
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
float permute(float x){return floor(mod(((x*34.0)+1.0)*x, 289.0));}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
float taylorInvSqrt(float r){return 1.79284291400159 - 0.85373472095314 * r;}
vec4 grad4(float j, vec4 ip){
const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
vec4 p,s;
p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
s = vec4(lessThan(p, vec4(0.0)));
p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www;
return p;
}
float snoise(vec4 v){
const vec2 C = vec2( 0.138196601125010504, // (5 - sqrt(5))/20 G4
0.309016994374947451); // (sqrt(5) - 1)/4 F4
// First corner
vec4 i = floor(v + dot(v, C.yyyy) );
vec4 x0 = v - i + dot(i, C.xxxx);
// Other corners
// Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI)
vec4 i0;
vec3 isX = step( x0.yzw, x0.xxx );
vec3 isYZ = step( x0.zww, x0.yyz );
// i0.x = dot( isX, vec3( 1.0 ) );
i0.x = isX.x + isX.y + isX.z;
i0.yzw = 1.0 - isX;
// i0.y += dot( isYZ.xy, vec2( 1.0 ) );
i0.y += isYZ.x + isYZ.y;
i0.zw += 1.0 - isYZ.xy;
i0.z += isYZ.z;
i0.w += 1.0 - isYZ.z;
// i0 now contains the unique values 0,1,2,3 in each channel
vec4 i3 = clamp( i0, 0.0, 1.0 );
vec4 i2 = clamp( i0-1.0, 0.0, 1.0 );
vec4 i1 = clamp( i0-2.0, 0.0, 1.0 );
// x0 = x0 - 0.0 + 0.0 * C
vec4 x1 = x0 - i1 + 1.0 * C.xxxx;
vec4 x2 = x0 - i2 + 2.0 * C.xxxx;
vec4 x3 = x0 - i3 + 3.0 * C.xxxx;
vec4 x4 = x0 - 1.0 + 4.0 * C.xxxx;
// Permutations
i = mod(i, 289.0);
float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x);
vec4 j1 = permute( permute( permute( permute (
i.w + vec4(i1.w, i2.w, i3.w, 1.0 ))
+ i.z + vec4(i1.z, i2.z, i3.z, 1.0 ))
+ i.y + vec4(i1.y, i2.y, i3.y, 1.0 ))
+ i.x + vec4(i1.x, i2.x, i3.x, 1.0 ));
// Gradients
// ( 7*7*6 points uniformly over a cube, mapped onto a 4-octahedron.)
// 7*7*6 = 294, which is close to the ring size 17*17 = 289.
vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ;
vec4 p0 = grad4(j0, ip);
vec4 p1 = grad4(j1.x, ip);
vec4 p2 = grad4(j1.y, ip);
vec4 p3 = grad4(j1.z, ip);
vec4 p4 = grad4(j1.w, ip);
// Normalise gradients
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
p4 *= taylorInvSqrt(dot(p4,p4));
// Mix contributions from the five corners
vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0);
vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4) ), 0.0);
m0 = m0 * m0;
m1 = m1 * m1;
return 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 )))
+ dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) ;
}
void main() {
float noise = snoise(vec4(position, 0.0));
vec3 newPos = position + 0.8 * normal * noise;
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
这里用接收 vec4 的 snoise 函数方便后续把 uTime 作为第4个参数,让形状动起来。
用法线 normal 作为颜色方便看变形后的形状。
// vertex shader
uniform float uTime;
varying vec3 vNormal;
float snoise(vec4 c){
// ...
}
void main() {
vNormal = normal;
float noise = snoise(vec4(position, 0.0));
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
// fragment shader
varying vec3 vNormal;
void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
gl_FragColor = vec4(vNormal, 1.0);
}
给 position 乘不同值,改变传给 noise 函数的顶点相邻程度,使形状变化更加“剧烈”。
// float noise = snoise(vec4(position * 1.0, 0.0));
// float noise = snoise(vec4(position * 0.3, 0.0));
float noise = snoise(vec4(position * 10.0, 0.0));
增加球体细分数,使球体上有更多顶点可以被用来偏移位置。
// const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereGeometry = new THREE.SphereGeometry(1, 200, 200);
将 uTime 作为第4个参数使形状实时发生变化。
float noise = snoise(vec4(position * 10.0, uTime * 0.2));
noise 值作为颜色
noise 数值除了用来偏移顶点坐标,还能用来设置颜色。将 vec3(noise) 灰度值颜色作为 vColor 传到片元着色器进行使用,那么 noise 值越大偏移高度越高、颜色越白;反之值越小高度越低、颜色越黑。
// vertex shader
uniform float uTime;
varying vec3 vNormal;
varying vec3 vColor;
void main() {
vNormal = normal;
float noise = snoise(vec4(position * 10.0, uTime * 0.2));
vColor = vec3(noise);
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
// fragment shader
varying vec3 vNormal;
varying vec3 vColor;
void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
// gl_FragColor = vec4(vNormal, 1.0);
gl_FragColor = vec4(vColor, 1.0);
}
也可以设置成 rgb 里的 red 值,就是红黑效果。
vColor = vec3(noise, 0.0, 0.0);
还可以把 noise 值设置到 HSV 模式里的 hue 色相值(第七篇里都讲过,不过那里用的 HSL 这里用的 HSV,需要注意 HSV=HSB!=HSL),然后转换回 rgb 模式。谷歌搜索 glsl hsv2rgb function
找到现成的实现,拷贝后就能使用,直接把 noise 作为 hue 会是五彩斑斓的效果,因为 noise 值0-1的话会把所有色相值覆盖到。虽然也挺好看,但不是我们这里想要的。
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vNormal = normal;
float noise = snoise(vec4(position * 10.0, uTime * 0.2));
// vColor = vec3(noise);
// vColor = vec3(noise, 0.0, 0.0);
vColor = hsv2rgb(vec3(noise, 1.0, 1.0));
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
缩小 noise 范围,使颜色变化不那么剧烈。
vColor = hsv2rgb(vec3(vNoise * 0.1, 1.0, 1.0));
最后微调 hue,改下饱和度,至效果满意即可。
vColor = hsv2rgb(vec3(noise * 0.1 + 0.04, 0.8, 1.0));
球形粒子系统
中心的效果完成后,接着在外面加一层球形的粒子,之前有群友在学第七篇时就有问到,这次终于可以讲解下(欢迎加我「xiaoaizhj
」,备注「可视化加群
」,一起交流)。
上一篇文章「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」对粒子系统已经做过介绍,这里就不再重复。
我们通过 BufferGeometry() 设置粒子的 position,使 radius 稍大于中心球体,这样能包裹着球体。这里想要粒子在球体上均匀分布,从网上找现成的公式即可。
const particleGeometry = new THREE.BufferGeometry();
const N = 4000;
const positions = new Float32Array(N * 3);
const inc = Math.PI * (3 - Math.sqrt(5));
const off = 2 / N;
const radius = 2;
for (let i = 0; i < N; i++) {
const y = i * off - 1 + off / 2;
const r = Math.sqrt(1 - y * y);
const phi = i * inc;
positions[3 * i] = radius * Math.cos(phi) * r;
positions[3 * i + 1] = radius * y;
positions[3 * i + 2] = radius * Math.sin(phi) * r;
}
particleGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
视频里用的是某博客文章里的公式,由于文章已看不到,这里放截图方便感兴趣的小伙伴看眼,总之把这里 python 代码改成上面 JS 代码即可。
我们也可以搜 evenly distribute points on a sphere
、fibonacci spiral sphere
等关键词,能找到其他大同小异的实现方式,下面是另一种方案,作为参考,可以看到效果都差不多。后续演示仍沿用第一种方案。
// 另一种生成球体上均匀粒子坐标的方式
for (let i = 0; i < N; i++) {
const k = i + 0.5;
const phi = Math.acos(1 - (2 * k) / N);
const theta = Math.PI * (1 + Math.sqrt(5)) * k;
const x = Math.cos(theta) * Math.sin(phi) * radius;
const y = Math.sin(theta) * Math.sin(phi) * radius;
const z = Math.cos(phi) * radius;
positions.set([x, y, z], i * 3);
}
材质用 ShaderMaterial,粒子颜色设置了透明度,然后和 particleGeometry 一起丢给 Points 就行。
const particleVertex = /* GLSL */ `
uniform float uTime;
void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = 6.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
`;
const particleFragment = /* GLSL */ `
void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
gl_FragColor = vec4(vec3(1.0), 0.6);
}
`;
const particleMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
},
vertexShader: particleVertex,
fragmentShader: particleFragment,
transparent: true,
blending: THREE.AdditiveBlending,
});
const particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);
function render() {
// ...
sphereMaterial.uniforms.uTime.value = time;
particleMaterial.uniforms.uTime.value = time;
}
不设置透明度的话效果如下。还是设置透明度、弱化粒子视觉效果后看着更舒服。
gl_FragColor = vec4(vec3(1.0), 1.0);
粒子上下波动
接着让粒子运动起来,可以通过对 y 坐标取 sin 值再加回到 y 上,这样同一高度的粒子会一起随 sin 波浪上下偏移,整体上就是波浪的效果。
// particleVertex
uniform float uTime;
void main() {
vec3 newPos = position;
newPos.y += 0.1 * sin(newPos.y * 6.0 + uTime);
// newPos.z += 0.05 * sin(newPos.y * 10.0 + uTime);
// vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
gl_PointSize = 6.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
还可以对 z 坐标进行一定偏移,虽然这里可能效果不明显,但常常会组合的对 xyz 进行 sin/cos 等操作,以后大家也会碰到(实际上后文就多次重复用到),所以这里先演示下。大家也可以自由发挥、随意尝试,不必局限本文所讲到的方法。
newPos.z += 0.05 * sin(newPos.y * 10.0 + uTime);
效果已经很漂亮了,大家可以休息下,喝杯奶茶或咖啡,好好欣赏享受下自己的成果。
背景用随机粒子进行点缀
上面都是油管教程里涉及到的内容,休息结束后,这次让我们加个餐、更进一步把原作里其他一些效果也简单实现下。
首先可以看到背景有点空,我们可以用随机粒子进行点缀,丰富画面效果。
通过 r 使得粒子在中心球体和球形粒子之外的范围,xyz 坐标随机在立方体空间内分布,这里都是简单的设置,所以怎么方便怎么来;sizes 控制每个粒子的随机大小并且会用作粒子移动速度,为了使值不为0,这样粒子速度不为0就不会停着不动,所以加了0.4。
const firefliesGeometry = new THREE.BufferGeometry();
const firefliesCount = 300;
const positions1 = new Float32Array(firefliesCount * 3);
const sizes = new Float32Array(firefliesCount);
for (let i = 0; i < firefliesCount; i++) {
const r = Math.random() * 5 + 5;
positions1[i * 3 + 0] = (Math.random() - 0.5) * r;
positions1[i * 3 + 1] = (Math.random() - 0.5) * r;
positions1[i * 3 + 2] = (Math.random() - 0.5) * r;
sizes[i] = Math.random() + 0.4;
}
firefliesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions1, 3)
);
firefliesGeometry.setAttribute("aSize", new THREE.BufferAttribute(sizes, 1));
顶点着色器里 gl_PointSize 乘上 aSize 改变大小,片元着色器里用每个粒子离中心的距离通过一个反比例函数 0.05/d-0.05*2.0
使得靠近中心为1,往外逐渐变成0,再设置成透明度值,从而实现出光斑、模糊圆形的效果。
const firefliesVertexShader = /* GLSL */ `
uniform float uTime;
attribute float aSize;
void main() {
vec3 newPos = position;
// newPos.y += sin(uTime * 0.5 + newPos.x * 100.0) * aSize * 0.2;
// newPos.x += sin(uTime * 0.5 + newPos.x * 200.0) * aSize * 0.1;
vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
gl_PointSize = 70.0 * aSize / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
`;
const firefliesFragmentShader = /* GLSL */ `
void main() {
float d = length(gl_PointCoord - vec2(0.5));
float strength = clamp(0.05 / d - 0.05 * 2.0, 0.0, 1.0);
gl_FragColor = vec4(vec3(1.0), strength);
}
`;
const firefliesMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
},
vertexShader: firefliesVertexShader,
fragmentShader: firefliesFragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial);
scene.add(fireflies);
通过曲线图看看透明度 strength 计算方式,当距离d=0.5时数值为0也就是圆圈边缘;d>0.5时数值为负数,通过clamp会变成0,完全透明;d=0.05/1.1=0.045时数值为1;d<0.045时数值大于1会被clamp取1,完全不透明,距离从0.045到0.5透明度快速降为0,从而实现模糊效果。
float d = length(gl_PointCoord - vec2(0.5));
float strength = clamp(0.05 / d - 0.05 * 2.0, 0.0, 1.0);
gl_FragColor = vec4(vec3(1.0), strength);
这种粒子实现方式还蛮常见。记得2022年4-5月份刚接触 shader 那会,碰上浙大125周年校庆,发现当年很特别官方搞了个求是星海
的网站效果,就是粒子系统为主,里面粒子颜色透明度就是用这种反比例函数实现。
通过 Chrome 浏览器的 Spector.js
插件就能看到这里粒子的着色器里的 starIntensity()
函数,就是如此实现的。
在该网站输入校友专业和名字、认证通过后会生成每个校友专属的由“灿若繁星”的浙大人姓名的粒子系统组成的效果。网页还在,但需要输入信息才能生成,所以非校友的话看不到,只能看这里截图。
扯回来,和上面球形粒子一样这里用 sin 函数使背景随机粒子也运动起来,参数可以任意修改看效果。
vec3 newPos = position;
newPos.y += sin(uTime * 0.5 + newPos.x * 100.0) * aSize * 0.2;
newPos.x += sin(uTime * 0.5 + newPos.x * 200.0) * aSize * 0.1;
显示文字
接着模仿原作在中心球体和球形粒子之间放上文字。这里通过在长度2宽度1的 Plane 上显示纹理图片实现,准备一张白色文字、背景透明、1024x512 的图片。下面是截图的效果,而不是原图,不要保存这张图去直接使用。原图已传到 postimg 这个网站,下面是链接直接用就行。如果后续想将本文的效果修改后放自己的个人网站,那么这里就可以和原作一样换成自己的名字。
将图片通过 TextureLoader().load()
加载后作为 uTexture
传给 shader,然后通过 uv 采样纹理图,再搭配 textMaterial 设置 transparent 即可显示文字。text mesh 通过设置 position.z=1.7 移动到所需位置。
const textGeometry = new THREE.PlaneGeometry(2, 1, 100, 100);
const textVertex = /* GLSL */ `
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 newPos = position;
// newPos.y += 0.06 * sin(newPos.x + uTime);
// newPos.x += 0.1 * sin(newPos.x * 2.0 + uTime);
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
`;
const textFragment = /* GLSL */ `
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec4 color = texture2D(uTexture, vUv);
gl_FragColor = color;
}
`;
const textMaterial = new THREE.ShaderMaterial({
vertexShader: textVertex,
fragmentShader: textFragment,
uniforms: {
uTime: { value: 0 },
uTexture: {
value: new THREE.TextureLoader().load("https://i.postimg.cc/nrSTmrZk/text.png"), // './assets/text.png',
},
},
transparent: true,
});
const text = new THREE.Mesh(textGeometry, textMaterial);
text.position.z = 1.7;
scene.add(text);
function render () {
// ...
sphereMaterial.uniforms.uTime.value = time;
particleMaterial.uniforms.uTime.value = time;
firefliesMaterial.uniforms.uTime.value = time;
textMaterial.uniforms.uTime.value = time;
}
接着再一次应用 sin 函数改变顶点位置使 plane 发生扭曲,顶点移动后 uv 也随之移动,用 uv 采样纹理后就会有文字飘浮的效果。
vec3 newPos = position;
newPos.y += 0.06 * sin(newPos.x + uTime);
newPos.x += 0.1 * sin(newPos.x * 2.0 + uTime);
上方光线
最后古柳观察到原作顶部有光线效果,于是也顺带实现了下,不过因为文章篇幅已经不短,加上这部分解释起来比前几个效果复杂些,就暂时不在本文讲了,后续其他文章有机会再讲。大家这里看眼效果即可,感兴趣可自行实现。
小结
至此,古柳带大家把这个自己入坑 shader 的酷炫效果“简单”复现了下,中间偷懒多次用了 sin 函数,所以其实并没有大家想的那么复杂。
虽然和原作比起来还是很粗陋,各种参数、粒子动画、颜色搭配都有很大优化空间,效果远远不如原作漂亮,但是作为一篇教程里的例子,或许已足够让大家学到些东西,其他的优化就留给大家去自由发挥了。
还是可惜原作早就看不到了,只能从油管视频里瞥见一些片段效果,无法自己去交互、去体验、去学习。但两年前初见 Pepyaka
、初识 shader 时的那份惊艳却伴随古柳至今,希望更多人看到本文后也能感受到那份惊艳,并能借本教程之力让那份惊艳不再局限于观赏,而是可以自己去一步步实现、一步步靠近,相信大家跟着古柳的系列文章一点点学下来,就不会再觉得这样的效果是自己无法理解、无法实现的了吧。
最后完整源码可见 Codepen。
相关阅读
「手把手带你入门 Three.js Shader 系列」目录如下:
- 「断更19个月,携 Three.js Shader 归来!(上)- 牛衣古柳 - 20230416」
- 「断更19个月,携 Three.js Shader 归来!(下)- 牛衣古柳 - 20230421」
- 「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」
- 「手把手带你入门 Three.js Shader 系列(八)- 牛衣古柳 - 20240229」
- 「手把手带你入门 Three.js Shader 系列(七)- 牛衣古柳 - 20230206」
- 「手把手带你入门 Three.js Shader 系列(六)- 牛衣古柳 - 20231220」
- 「手把手带你入门 Three.js Shader 系列(五)- 牛衣古柳 - 20231126」
- 「手把手带你入门 Three.js Shader 系列(四)- 牛衣古柳 - 20231121」
- 「手把手带你入门 Three.js Shader 系列(三)- 牛衣古柳 - 20230725」
- 「手把手带你入门 Three.js Shader 系列(二)- 牛衣古柳 - 20230716」
- 「手把手带你入门 Three.js Shader 系列(一)- 牛衣古柳 - 20230515」
照例
如果你喜欢本文内容,欢迎以各种方式支持,这也是对古柳输出教程的一种正向鼓励!
最后欢迎加入「可视化交流群」
,进群多多交流,对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj
,备注「可视化加群」
即可。
欢迎关注古柳的公众号「牛衣古柳」
,并设置星标,以便第一时间收到更新。