效果图
关键项目依赖
- vite@5.2.10
- vite-plugin-glsl@1.3.0
- vite-plugin-lygia@1.0.4
- lygia@1.1.3
- three@0.166.1
- @react-three/fiber@8.14.3
- @react-three/drei@9.86.3
- maath@0.10.8
第一步:安装依赖和初始化3d场景
项目基于vite+react模版创建
- 安装依赖基础three、@react-three/fiber、@react-three/drei
yarn add three @react-three/fiber @react-three/drei
- 初始化场景
App.tsx
import { Canvas } from "@react-three/fiber";
import "./App.css";
import Render from "./Render";
function App() {
return (
<>
<Canvas camera={{ far: 200, position: [0, 0, 10] }}>
<Render />
</Canvas>
</>
);
}
export default App;
Render.tsx
const Sphere = () => {
return (
<mesh rotation={[0, 0, 0]} scale={1.2}> // 添加一个网格体
<sphereGeometry args={[4, 320, 320]} /> // 添加一个球体
<meshPhysicalMaterial color={"blue"} /> // 给球体添加一个蓝色的物理网格材质
</mesh>
);
};
const Render = () => {
return (
<>
<Sphere />
</>
);
};
export default Render;
此刻我们可以看到场景中有一个黑色的圆
tips:由于我们使用的是一个物理网格材质,并且场景当中没有任何的光照,所以看到的是一个黑色的球
效果图
第二步:给场景添加灯光
App.tsx
import { Canvas } from "@react-three/fiber";
import "./App.css";
import Render from "./Render";
import { Environment, Lightformer } from "@react-three/drei";
function App() {
return (
<>
<Canvas camera={{ far: 200, position: [0, 0, 10] }}>
<Render />
<Environment>
<Lightformer
color={"white"}
intensity={1}
position={[0, 0, -10]}
scale={[10, 50, 1]}
onUpdate={(self) => self.lookAt(0, 0, 0)}
form={"circle"}
/>
// 一个发光圆球大小为10,50,1,在0,0,4处照亮物体,光照强度为1,颜色为白色
<Lightformer
color={"white"}
intensity={1}
position={[0, 0, 4]}
scale={[10, 50, 1]}
onUpdate={(self) => self.lookAt(0, 0, 0)}
form={"circle"}
/>
</Environment>
</Canvas>
</>
);
}
export default App;
这个时候我们可以得到一个蓝色的小球
效果图
第三步:给材质添加shader - 重点
- 安装lygia、vite-plugin-glsl(解析glsl语法)、vite-plugin-lygia(在glsl文件中加载lygia)
yarn add lygia
yarn add vite-plugin-glsl vite-plugin-lygia -D
- 配置vite.config.ts文件
import lygia from 'vite-plugin-lygia';
import topLevelAwait from 'vite-plugin-top-level-await';
export default defineConfig({
plugins: [
react(),
lygia({ libraryPath: '' }),
glsl({
include: [
'**/*.glsl',
'**/*.wgsl',
'**/*.vert',
'**/*.frag',
'**/*.vs',
'**/*.fs'
]
})
],
});
-
当前目录下面创建glsl目录,并新增vertexHead.vert、vertexBody.vert、fragmentHead.frag、fragmentBody.frag四个shader文件
-
修改threejs物理网格材质的shader(着色器)代码
// Render.tsx
import {
WebGLProgramParametersWithUniforms,
WebGLRenderer,
Vector2,
MeshPhysicalMaterial,
} from "three";
import { useRef, useState } from "react";
import { useFrame } from "@react-three/fiber";
// 导入新建的文件
import vertexHead from "./glsl/vertexHead.vert";
import vertexBody from "./glsl/vertexBody.vert";
import fragmentHead from "./glsl/fragmentHead.frag";
import fragmentBody from "./glsl/fragmentBody.frag";
const Sphere = () => {
// 材质对象
const shaderRef = useRef<MeshPhysicalMaterial>(null);
// 鼠标位置信息[0,0]
const [mouse, setMouse] = useState<Vector2>(new Vector2());
// 根据屏幕的刷新频率重复调用
useFrame(({ clock, pointer }) => {
setMouse(new Vector2(pointer.x, pointer.y));
// 给shader中的uniform变量设置值
if (shaderRef.current!.userData.shader) {
shaderRef.current!.userData.shader.uniforms.uTime = {
value: clock.getElapsedTime(),
};
shaderRef.current!.userData.shader.uniforms.uMouse = {
value: mouse,
};
}
});
// 着色器在绘制之前进行修改
const onBeforeCompile = (shader: WebGLProgramParametersWithUniforms) => {
// 给shader中的uniform变量设置初始值
shader.uniforms = {
...shader.uniforms,
uTime: { value: 0 },
uMouse: { value: mouse },
};
// 给默认的片段着色器添加自定义着色器代码
shader.fragmentShader = shader.fragmentShader
.replace("#include <common>\n", `#include <common>\n${fragmentHead}\n`)
.replace(
"#include <dithering_fragment>\n",
`#include <dithering_fragment>\n${fragmentBody}\n`,
);
// 给默认的顶点着色器添加自定义着色器代码
shader.vertexShader = shader.vertexShader
.replace(`#include <common>\n`, `#include <common>\n${vertexHead}\n`)
.replace(
`#include <fog_vertex>\n`,
`#include <fog_vertex>\n${vertexBody}\n`,
);
// 将修改后的着色器放到userData下面
shaderRef.current!.userData.shader = shader;
};
return (
<mesh rotation={[0, 0, 0]} scale={1.2}>
<sphereGeometry args={[4, 320, 320]} />
<meshPhysicalMaterial
ref={shaderRef}
color={"blue"}
onBeforeCompile={onBeforeCompile}
/>
</mesh>
);
};
const Render = () => {
return (
<>
<Sphere />
</>
);
};
export default Render;
- 编写vertexShader代码
噪波-黑白灰的的图(在shader中可以简单的理解为很多0-1之间的数的集合)
// vertexHead.vert
#include "lygia/generative/cnoise.glsl"; // 噪波
#include "lygia/space/rotate.glsl"; // 旋转
// 通过js传过来的变量
uniform float uTime; // 时间(会一直累加)
uniform vec2 uMouse; // 鼠标的位置x,y
// 与fragmentShader共享
varying vec3 vPosition; // 顶点数据
varying vec2 vUv; // uv数据
// vertexBody.vert
// 设置定点沿y轴上下移动
mvPosition.y += (cos(normal.x * 10.0 + uTime) - 0.5) * 0.1;
// 沿z轴移动
mvPosition.z += sin(normal.z * 20.0 + uTime) * 0.2;
// 根据噪波移动
mvPosition += (cnoise(mvPosition.xyz + uTime) - 0.5) * 0.05 ;
// 给gl_Position重新赋值
gl_Position = projectionMatrix * mvPosition;
vPosition = mvPosition.xyz;
vNormal = normal;
vUv = uv;
此时蓝色的小球表面会上下起伏运动
效果图
- 编写fragmentShader代码
// fragmentHead.frag
// js传递过来的变量
uniform float uTime;
uniform vec2 uMouse;
// 与顶点着色器共享的数据
varying vec3 vPosition;
varying vec2 vUv;
#include "lygia/generative/cnoise.glsl"; // 噪波
// 根据一个浮点数据生成色块
vec3 palette(float t) {
vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.263, 0.416, 0.557);
return a + b * cos(6.28318 * (c * t + d));
}
// fragmentBody.frag
float n = cnoise(vPosition); // 生成噪波
vec3 color = palette(n);
gl_FragColor = mix(gl_FragColor, vec4(color, 1.0), -1.0);
到这里我们shader部分已经写完了,这个时候我们可以得到一个表面在运动,且有很多色斑的圆
效果图
- 修改材质数据和调整光照
材质配置属性:threejs.org/docs/index.…
// App.tsx
...
<Environment>
<Lightformer
color={"white"}
intensity={2}
position={[0, 0, -10]}
scale={[10, 50, 1]}
onUpdate={(self) => self.lookAt(0, 0, 0)}
form={"circle"}
/>
<Lightformer
color={"white"}
intensity={6}
position={[0, 0, 4]}
scale={[10, 50, 1]}
onUpdate={(self) => self.lookAt(0, 0, 0)}
form={"circle"}
/>
</Environment>
...
// Render.tsx
...
<meshPhysicalMaterial
ref={shaderRef}
color={"black"}
emissive={"black"}
envMap={null}
envMapIntensity={1.0}
roughness={0.5}
thickness={1.0}
ior={1.52}
vertexColors
onBeforeCompile={onBeforeCompile}
/>
...
效果图
到这里我们球的动画就大致完成了
第四步:设置背景
matcap贴图,将物体的漫反射、环境、高光、反射等信息输出到图片上
// Render.tsx
...
const Background = () => {
const matcap = useTexture("/matcap/11.png");
return (
<mesh position={[0, 0, 0]}>
// 用一个很大的球体做背景
<sphereGeometry args={[14, 32, 32]} />
// 使用matcap做贴图
<meshMatcapMaterial matcap={matcap} side={BackSide} />
</mesh>
);
};
...
const Render = () => {
return (
<>
<Sphere />
<Background />
</>
);
};
第五步:设置鼠标交互
先安装maath依赖,用于处理过渡动画
yarn add maath
// App.tsx
function App() {
return (
<>
<Canvas camera={{ far: 200, position: [0, 0, 10] }}>
...
// 添加相机控制器,将Render组件放到控制器里面
<PresentationControls
snap
global
zoom={0.8}
rotation={[0, 0, 0]}
polar={[0, Math.PI / 4]}
azimuth={[-Math.PI / 4, Math.PI / 4]}
>
<Render />
</PresentationControls>
</Canvas>
</>
);
}
// Render.tsx
...
const Render = () => {
useFrame(({ pointer, camera }, delta) => {
// 相机位置跟随鼠标位置发生位移
easing.damp3(camera.position, [-pointer.x, -pointer.y, 10], 0.2, delta);
camera.lookAt(0, 0, 0);
});
return (
<>
<Sphere />
<Background />
</>
);
};
最后一步加上文字
// Render.tsx
...
const TextCom = () => {
return (
<>
<Html center prepend>
<div className="text">hello world!</div>
</Html>
</>
);
};
...
效果图