Three js 着色器材质 shader 电脑版发表于:2023/7/20 11:33 ![](https://img.tnblog.net/arcimg/hb/305f7258222b48778049d4a891c85464.png) >#Three js 着色器材质 shader [TOC] 什么是 GLSL? ------------ tn2>GLSL 代表 openGL Shading Language,它是着色器程序的特定标准,您将在接下来的章节中看到。根据硬件和操作系统,还有其他类型的着色器。在这里,我们将使用由Khronos Group监管的 openGL 规范。了解 OpenGL 的历史有助于理解其大部分奇怪的约定,为此我建议您查看:openglbook.com/chapter-0-preface-what-is-opengl.html 着色器材质(ShaderMaterial) ------------ tn2>使用自定义shader渲染的材质。 shader是一个用GLSL编写的小程序 ,在GPU上运行。 您可能需要使用自定义shader,如果你要: ——要实现内置 materials 之外的效果。 ——将许多对象组合成单个BufferGeometry以提高性能。 tn>ShaderMaterial 只有使用 WebGLRenderer 才可以绘制正常, 因为 `vertexShader` 和 `fragmentShader` 属性中GLSL代码必须使用WebGL来编译并运行在GPU中。 顶点着色器和片元着色器 ------------ ### 什么是顶点着色器(vertexShader) tn2>它接收attributes, 计算/操纵每个单独顶点的位置,并将其他数据(varyings)传递给片元着色器。 ### 什么是片元着色器(fragmentShader) tn2>它设置渲染到屏幕的每个单独的片元(像素)的颜色。 ### 举例 tn2>我们通过自定义的着色器渲染一个平面材质。 | 变量 | 描述 | | ------------ | ------------ | | `gl_Position` | OpenGL中的一个内置变量,用于表示顶点在裁剪空间中的位置。 | | `vec4` | 表示一个四维向量,由x、y、z和w四个分量组成 。 | | `position` | 表示当前的位置 | | `gl_FragColor` | 是OpenGL中的一个内置变量,用于设置片元的颜色 。它是一个vec4类型的变量,由x、y、z和w四个分量组成 。其中,x、y、z分量表示颜色的红、绿、蓝分量,w分量表示透明度 。 | tn2>这里的`gl_FragColor`我设置的是黄色的面板。 ```javascript // 创建着色器材质 const shaderMaterial = new THREE.ShaderMaterial({ // 顶点着色器 vertexShader: ` void main(){ gl_Position = vec4( position, 1.0 ) ; } `, // 片元着色器 fragmentShader: ` void main(){ gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); } `, }); // 创建平面 const floor = new THREE.Mesh( new THREE.PlaneGeometry(1, 1, 64, 64), shaderMaterial ); console.log(floor); // 添加 scene.add(floor); ``` ![](https://img.tnblog.net/arcimg/hb/5c6b51ae309c412ca2a88918435aa453.png) tn2>但这样会发现个问题,由于坐标是一成不变的所以不论你如何移动旋转它将都这样显示,所以的我们在旋转移动鼠标的同时还需要乘以如下几个矩阵:`<投影矩阵>*<视图矩阵>*<模型矩阵>*<顶点坐标>` 代码改成如下所示,再次运行 ```javascript gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ; ``` ![](https://img.tnblog.net/arcimg/hb/85e38b754dd644ec9ef06d00ac9be88a.png) tn2>相乘的顺序不能改变。物体本身拥有一个坐标系,叫本地坐标系。把物体放到世界坐标系中,采用了模型矩阵,就是执行缩放、平移、旋转操作的过程。此时物体就具有了世界坐标系。 再加入上帝之眼,就是视图矩阵,包括视点坐标,观察点坐标,和上方向。现在只差最后一步–投影矩阵,物体就可以呈现出来了。 目前显示设备都是二维平面的,所以需要投影矩阵来转换物体。投影矩阵通常分为平行投影和透视投影。 ![](https://img.tnblog.net/arcimg/hb/df28fad3c58d430c83940b95dfc101e7.png) ![](https://img.tnblog.net/arcimg/hb/703ae2258c344be194c5133c8886d096.png) | 名称 | 变量 | | ------------ | ------------ | | 投影矩阵 | projectionMatrix | | 视图矩阵 | viewMatrix | | 模型矩阵 | modelMatrix | 完整代码 ------------ ```javascript import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; import gsap from "gsap"; import * as dat from "dat.gui"; // 目标:认识shader //创建gui对象 const gui = new dat.GUI(); // console.log(THREE); // 初始化场景 const scene = new THREE.Scene(); // 创建透视相机 const camera = new THREE.PerspectiveCamera( 90, window.innerHeight / window.innerHeight, 0.1, 1000 ); // 设置相机位置 // object3d具有position,属性是1个3维的向量 camera.position.set(0, 0, 2); // 更新摄像头 camera.aspect = window.innerWidth / window.innerHeight; // 更新摄像机的投影矩阵 camera.updateProjectionMatrix(); scene.add(camera); // 加入辅助轴,帮助我们查看3维坐标轴 const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper); // 加载纹理 // 创建纹理加载器对象 const textureLoader = new THREE.TextureLoader(); const texture = textureLoader.load("./texture/ca.jpeg"); const params = { uFrequency: 10, uScale: 0.1, }; // const material = new THREE.MeshBasicMaterial({ color: "#00ff00" }); // 创建着色器材质 const shaderMaterial = new THREE.ShaderMaterial({ // 顶点着色器 vertexShader: ` void main(){ gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ; } `, // 片元着色器 fragmentShader: ` void main(){ gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); } `, }); // // 创建平面 const floor = new THREE.Mesh( new THREE.PlaneGeometry(1, 1, 64, 64), shaderMaterial ); console.log(floor); scene.add(floor); // 初始化渲染器 const renderer = new THREE.WebGLRenderer({ alpha: true }); // renderer.shadowMap.enabled = true; // renderer.shadowMap.type = THREE.BasicShadowMap; // renderer.shadowMap.type = THREE.VSMShadowMap; // 设置渲染尺寸大小 renderer.setSize(window.innerWidth, window.innerHeight); // 监听屏幕大小改变的变化,设置渲染的尺寸 window.addEventListener("resize", () => { // console.log("resize"); // 更新摄像头 camera.aspect = window.innerWidth / window.innerHeight; // 更新摄像机的投影矩阵 camera.updateProjectionMatrix(); // 更新渲染器 renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器的像素比例 renderer.setPixelRatio(window.devicePixelRatio); }); // 将渲染器添加到body document.body.appendChild(renderer.domElement); // 初始化控制器 const controls = new OrbitControls(camera, renderer.domElement); // 设置控制器阻尼 controls.enableDamping = true; // 设置自动旋转 // controls.autoRotate = true; const clock = new THREE.Clock(); function animate(t) { const elapsedTime = clock.getElapsedTime(); // console.log(elapsedTime); requestAnimationFrame(animate); // 使用渲染器渲染相机看这个场景的内容渲染出来 renderer.render(scene, camera); } animate(); ``` tn2>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>Document</title> <link rel="stylesheet" href="./assets/css/style.css" /> </head> <body> <script src="./main/main.js" type="module"></script> </body> </html> ``` tn2>style.css ```css * { margin: 0; padding: 0; } body { background-color: #1e1a20; } ::-webkit-scrollbar { display: none; } .page { display: flex; justify-content: center; height: 100vh; padding: 0 10%; color: #fff; flex-direction: column; } .page h1 { margin: 60px 0; font-size: 40px; } .page h3 { font-size: 30px; } ``` 着色器插件安装与导入 ------------ tn2>由于OpenGL是一个单独的语言,所以我们更希望它能够单独弄成一个`.glsl`文件,在需要的时候进行导入。 首先我们创建`shader/basic`文件夹,并创建相关的文件分别存放顶点着色器和片元着色器的代码。 ![](https://img.tnblog.net/arcimg/hb/f089d30bfd7f44e9ab48f32a12ee7df8.png) tn2>fragment.glsl ```javascript void main(){ gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); } ``` tn2>vertex.glsl ```javascript void main(){ gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ; } ``` tn2>在main.js中我们对其进行导入,修改成如下代码。 ```javascript // 顶点着色器 import basicVertexShader from "../shader/basic/vertex.glsl"; // 片元着色器 import basicFragmentShader from "../shader/basic/fragment.glsl"; ... const shaderMaterial = new THREE.ShaderMaterial({ // 顶点着色器 vertexShader: basicVertexShader, // 片元着色器 fragmentShader: basicFragmentShader, }); ``` tn2>效果与前面运行一样。 如果想对gl的代码有所高亮显示,可以安装该插件。 ![](https://img.tnblog.net/arcimg/hb/47f0fd3ce7c34c178c7423e7a0353f03.png) ![](https://img.tnblog.net/arcimg/hb/0886a0d42fae43c1a2c8f4fd6aa95514.png) 原始着色器材质(RawShaderMaterial) ------------ tn2>此类的工作方式与ShaderMaterial类似,不同之处在于内置的uniforms和attributes的定义不会自动添加到GLSL shader代码中。 (修改成如下代码) ```javascript // 顶点着色器 import basicVertexShader from "../shader/raw/vertex.glsl"; // 片元着色器 import basicFragmentShader from "../shader/raw/fragment.glsl"; ... const rawshaderMaterial = new THREE.RawShaderMaterial({ // 顶点着色器 vertexShader: basicVertexShader, // 片元着色器 fragmentShader: basicFragmentShader, }); // 创建平面 const floor = new THREE.Mesh( new THREE.PlaneGeometry(1, 1, 64, 64), rawshaderMaterial ); ``` tn2>注意这里我更改了glsl的路径,并在相应的路径下创建了文件,需要注意的是需要自行定义变量。 ```javascript // fragment.glsl void main(){ gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); } ``` ```javascript attribute vec3 position; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; void main(){ gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ; } ``` ![](https://img.tnblog.net/arcimg/hb/54aa3dfeba6b41ebb5e504bcf41e75c0.png) ### 着色器材质的变量 tn2>每个着色器材质都可以指定两种不同类型的shaders,他们是顶点着色器和片元着色器(Vertex shaders and fragment shaders)。 ● 顶点着色器首先运行; 它接收attributes, 计算/操纵每个单独顶点的位置,并将其他数据(varyings)传递给片元着色器。 ● 片元(或像素)着色器后运行; 它设置渲染到屏幕的每个单独的“片元”(像素)的颜色。 shader中有三种类型的变量: uniforms, attributes, 和 varyings ● Uniforms是所有顶点都具有相同的值的变量。 比如灯光,雾,和阴影贴图就是被储存在uniforms中的数据。 uniforms可以通过顶点着色器和片元着色器来访问。 ● Attributes 与每个顶点关联的变量。例如,顶点位置,法线和顶点颜色都是存储在attributes中的数据。attributes 只 可以在顶点着色器中访问。 ● Varyings 是从顶点着色器传递到片元着色器的变量。对于每一个片元,每一个varying的值将是相邻顶点值的平滑插值。 ### 着色器传值 tn2>如何从顶点着色器的值传入到片元着色器中? 举例我们可以通过定义uv变量来进行传值。 ```javascript // vertex.glsl // 设置绘制的高低精度 precision lowp float; attribute vec3 position; attribute vec2 uv; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; varying vec2 vUv; // 设置精度 // highp -2^16 ~ 2^16 // mediump -2^10 ~ 2^10 // lowp -2^8 ~ 2^8 void main(){ vUv = uv; gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ; } ``` ```javascript // fragment.glsl // 设置绘制的高低精度 precision lowp float; varying vec2 vUv; void main(){ gl_FragColor = vec4(vUv, 0.0, 1.0); } ``` ![](https://img.tnblog.net/arcimg/hb/baf01b8a48f84097a8b71650016325fd.png) ### 设置线框 tn2>通过设置原始着色器的`wireframe`属性为true可以显示图像的像框。 ```javascript // 创建着色器材质 const rawshaderMaterial = new THREE.RawShaderMaterial({ // 顶点着色器 vertexShader: basicVertexShader, // 片元着色器 fragmentShader: basicFragmentShader, // 设置线框模式 wireframe: true, }); // 创建平面 const floor = new THREE.Mesh( new THREE.PlaneGeometry(1, 1), rawshaderMaterial ); ``` ![](https://img.tnblog.net/arcimg/hb/952e0ff5f2e2463f99fc57159d753134.png) ### 设置图像绘制的坐标 tn2>设置Position位置x为1,z为1的情况下如何进行显示。 修改vertex.glsl ```javascript // 设置绘制的高低精度 precision lowp float; attribute vec3 position; attribute vec2 uv; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; varying vec2 vUv; void main(){ vUv = uv; vec4 modelPosition = modelMatrix * vec4( position, 1.0 ); modelPosition.x += 1.0; modelPosition.z += 1.0; gl_Position = projectionMatrix * viewMatrix * modelPosition ; } ``` ![](https://img.tnblog.net/arcimg/hb/5673f78f85174adfa154cabfe150a7bf.png) ### 设置旋转 ```javascript void main(){ vUv = uv; vec4 modelPosition = modelMatrix * vec4( position, 1.0 ); // modelPosition.x += 1.0; // modelPosition.z += 1.0; modelPosition.z += modelPosition.x; gl_Position = projectionMatrix * viewMatrix * modelPosition ; } ``` ![](https://img.tnblog.net/arcimg/hb/c653844113d04a4ba92fb8a67b80a124.png) tn2>或者我们可以使用一些数学函数,例如:`sin()` ```javascript // 或数学函数 modelPosition.z += sin(modelPosition.x* 10.0); ``` ### 绘制水平弯曲波浪形状 ```javascript void main(){ vUv = uv; vec4 modelPosition = modelMatrix * vec4( position, 1.0 ); // 或数学函数 modelPosition.z += sin(modelPosition.x * 100.0)*0.1; gl_Position = projectionMatrix * viewMatrix * modelPosition ; } ``` ```javascript const rawshaderMaterial = new THREE.RawShaderMaterial({ // 顶点着色器 vertexShader: basicVertexShader, // 片元着色器 fragmentShader: basicFragmentShader, // 设置线框模式 // wireframe: true, // 传递纹理 side: THREE.DoubleSide, }); ``` ![](https://img.tnblog.net/arcimg/hb/97d2a9d741d34d40b8256768ee48809a.png) ### 绘制y轴的玻璃形状 ```javascript void main(){ vUv = uv; vec4 modelPosition = modelMatrix * vec4( position, 1.0 ); // 或数学函数 modelPosition.z += sin(modelPosition.x * 10.0)*0.05; // 加上y轴的波动 modelPosition.z += sin(modelPosition.y * 10.0)*0.05; gl_Position = projectionMatrix * viewMatrix * modelPosition ; } ``` ![](https://img.tnblog.net/arcimg/hb/5a6f0db9b0644df6866ed4e5aba66279.png) tn2>通过传入z的位置来设置颜色。 ```javascript // 设置绘制的高低精度 precision lowp float; attribute vec3 position; attribute vec2 uv; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; varying vec2 vUv; varying float vElevation; // 设置精度 // highp -2^16 ~ 2^16 // mediump -2^10 ~ 2^10 // lowp -2^8 ~ 2^8 void main(){ vUv = uv; vec4 modelPosition = modelMatrix * vec4( position, 1.0 ); // 或数学函数 modelPosition.z += sin(modelPosition.x * 10.0)*0.05; // 加上y轴的波动 modelPosition.z += sin(modelPosition.y * 10.0)*0.05; vElevation = modelPosition.z; gl_Position = projectionMatrix * viewMatrix * modelPosition ; } ``` ```javascript // 设置绘制的高低精度 precision lowp float; varying vec2 vUv; varying float vElevation; void main(){ // gl_FragColor = vec4(vUv, 0.0, 1.0); float letvElevation = vElevation + 0.05 * 10.0; gl_FragColor = vec4(1.0*letvElevation,0.0, 0.0, 1.0); } ``` ![](https://img.tnblog.net/arcimg/hb/178ce9a1d7814f358f466c706e68cc17.png) 让图像动起来! ------------ tn2>通过在传入时间变量,并更具时间变量的不同的移动它的z轴,让其产生波动的效果。 ```javascript const rawshaderMaterial = new THREE.RawShaderMaterial({ // 顶点着色器 vertexShader: basicVertexShader, // 片元着色器 fragmentShader: basicFragmentShader, // 设置线框模式 // wireframe: true, // 传递纹理 side: THREE.DoubleSide, // 传递参数 uniforms: { uTime: { value: 0 }, }, }); ... function animate(t) { const elapsedTime = clock.getElapsedTime(); // 传入时间 rawshaderMaterial.uniforms.uTime.value = elapsedTime; requestAnimationFrame(animate); // 使用渲染器渲染相机看这个场景的内容渲染出来 renderer.render(scene, camera); } ``` ```javascript // 获取时间 uniform float uTime; ... // 或数学函数 modelPosition.z += sin((modelPosition.x + uTime) * 10.0)*0.05; // 加上y轴的波动 modelPosition.z += sin((modelPosition.y + uTime) * 10.0)*0.05; ``` ![](https://img.tnblog.net/arcimg/hb/f4962803ca4e41088f263e3ad0afd5b5.png) tn2>我们再添加2022残奥会的图片,在`fragment.glsl`文件中进行修改并添加纹理。 ![](https://img.tnblog.net/arcimg/hb/ec47f8978f3745778a1b49e784ca0656.jpeg) ```javascript import image from '../assets/ca.jpeg' ... const texture = textureLoader.load(image); ... const rawshaderMaterial = new THREE.RawShaderMaterial({ ... // 传递参数 uniforms: { uTime: { value: 0 }, uTexture: { value: texture, }, }, }); ``` ```javascript // 设置绘制的高低精度 precision lowp float; varying vec2 vUv; varying float vElevation; uniform sampler2D uTexture; void main(){ // 根据UV,取出对应的颜色 vec4 textureColor = texture2D(uTexture, vUv); float letvElevation = vElevation + 0.05 * 10.0; textureColor.rgb *= letvElevation; gl_FragColor = textureColor; //vec4(textureColor.rgb, 1.0); } ``` tn>需要注意的是,在进行build后图片的路径可能会有所改变导致加载不了图片。 ![](https://img.tnblog.net/arcimg/hb/251e88bb21fa4943a50f1b6aa3cf762a.png)