用threejs+rtf手写hello world的第一天

286 阅读4分钟

效果图

Jul-26-2024 10-21-02.gif

关键项目依赖

  • 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模版创建

  1. 安装依赖基础three、@react-three/fiber、@react-three/drei
yarn add three @react-three/fiber @react-three/drei
  1. 初始化场景
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:由于我们使用的是一个物理网格材质,并且场景当中没有任何的光照,所以看到的是一个黑色的球

效果图

image.png

第二步:给场景添加灯光

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;

这个时候我们可以得到一个蓝色的小球

效果图

image.png

第三步:给材质添加shader - 重点

  1. 安装lygia、vite-plugin-glsl(解析glsl语法)、vite-plugin-lygia(在glsl文件中加载lygia)
yarn add lygia
yarn add vite-plugin-glsl vite-plugin-lygia -D
  1. 配置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'
      ]
    })
  ],
});
  1. 当前目录下面创建glsl目录,并新增vertexHead.vert、vertexBody.vert、fragmentHead.frag、fragmentBody.frag四个shader文件

  2. 修改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;
  1. 编写vertexShader代码

噪波-黑白灰的的图(在shader中可以简单的理解为很多0-1之间的数的集合)

连接:lygia.xyz/generative/…

image.png

// 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;

此时蓝色的小球表面会上下起伏运动

效果图

Jul-26-2024 14-00-51.gif

  1. 编写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部分已经写完了,这个时候我们可以得到一个表面在运动,且有很多色斑的圆

效果图

Jul-26-2024 14-13-26.gif

  1. 修改材质数据和调整光照

材质配置属性: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}
/>
...

效果图

Jul-26-2024 14-23-37.gif

到这里我们球的动画就大致完成了

第四步:设置背景

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>
    </>
  );
};
...

效果图

Jul-26-2024 10-21-02.gif