threejs学习笔记(七)

提高渲染真实性

色调映射Tone Mapping

色调映射,直接作用于webGL渲染器,意在将低画质的贴图效果模拟成高画质贴图效果,官方文档的描述如下:
在这里插入图片描述
通常来说,如果我们使用HDR材质的环境贴图,会导致加载慢等问题,但是使用LDR(Low Dynamic Range)的贴图(比如使用jpg格式柱状全景图),又会觉得其效果不够好,所以threejs贴心的准备了这一选项

const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

// tone mapping

gui.add(renderer, 'toneMapping', {
    No: THREE.NoToneMapping,
    Linear: THREE.LinearToneMapping,
    Reinhard: THREE.ReinhardToneMapping,
    Cineon: THREE.CineonToneMapping,
    ACESFilmic: THREE.ACESFilmicToneMapping,
    AgX: THREE.AgXToneMapping,
    neutral: THREE.NeutralToneMapping,
    custom: THREE.CustomToneMapping
})

默认情况下,webGL渲染器的色调映射模式为NoToneMapping,通过gui来控制每一种toneMapping的值方便我们测试其效果:

在这里插入图片描述
经过测试大致可以得到以下结论:

  • THREE.NoToneMapping

描述:禁用色调映射。
特点:直接输出场景的颜色值,不进行任何调整或压缩。
应用场景:适用于不需要HDR效果的情况。

  • THREE.LinearToneMapping

描述:线性色调映射。
特点:简单地将颜色值从线性空间转换到sRGB空间。
应用场景:基础的色彩管理,适合于不需要复杂HDR处理的场合。

  • THREE.ReinhardToneMapping

描述:基于Reinhard的色调映射算法。
特点:能够有效地处理非常亮的区域,同时保持暗部细节。
应用场景:广泛应用于游戏和CGI中,因为它能够很好地平衡亮度和对比度。

  • THREE.CineonToneMapping

描述:模仿电影胶片的效果。
特点:提供了更柔和的过渡效果,适合追求电影感的画面。
应用场景:电影制作和高端视觉特效项目。

  • THREE.ACESFilmicToneMapping

描述:基于Academy Color Encoding System (ACES) 的色调映射。
特点:模拟了现代数字电影摄影机的响应曲线,提供了非常自然的视觉效果。
应用场景:专业级视频制作和要求极高的图形应用。

  • THREE.AgXToneMapping

描述:一种模拟胶片感光特性的算法。
特点:提供了类似于传统黑白胶片的效果。
应用场景:艺术创作和特定风格化的项目。
THREE.NeutralToneMapping

描述:中性色调映射。
特点:尽量保持颜色的真实性,减少过度处理带来的失真。
应用场景:需要真实色彩再现的应用。

  • THREE.CustomToneMapping

描述:自定义色调映射。
特点:允许开发者实现自己的色调映射逻辑。
应用场景:当上述预设不能满足特定需求时,可以使用此选项来实现个性化的色调处理。

此外,我们还可以使用toneMappingExposure来控制色调的曝光程度,画面的体现上来看就是对于亮度的调节而已

抗锯齿(Anti-Aliasing)

首先要明白为什么会有锯齿(aliasing),计算机的渲染是按像素进行的,像素的最小单位就是1x1的矩形,假如要渲染一个非矩形的形状,计算机就可能会因为计算有误导致某个像素没有出来,进而形成阶梯状的边缘,比如:
在这里插入图片描述
在这里插入图片描述
所以,为了提高画质,避免这些边缘的阶梯状效果,就需要抗锯齿技术来对画面进行优化

SSAA

超采样抗锯齿,全名为Super-Sampling-Anti-Aliasing,它的原理简单粗暴,就是将原本要渲染的分辨率放大一倍,渲染后再进行缩放;比如要将场景渲染到1920 * 1080的画面上,开启抗锯齿后就是先将场景渲染到3840 * 2160的画面上后再缩放至1920 * 1080,这样子的抗锯齿效果显著,但是性能开销显然就非常大了

2X SSAA,就代表渲染到2倍于原始分辨率的场景,每一个像素相当于放大了 2 x 2 = 4倍,渲染的像素总数就是原来的4倍

4X SSAA,则代表每个像素变为原来的 4 * 4 = 16倍,渲染的像素总数就是原来的16倍

优点:

  • 提供高质量的抗锯齿效果。
  • 能够消除几乎所有的锯齿现象。

缺点:

  • 计算成本非常高,渲染时间显著增加。
  • 不适用于性能要求较高的场景。
MSAA

Multi-Sample Anti-Aliasing,多重采样抗锯齿,原理是在每个像素周围进行多次采样来减少锯齿现象,实现上相对简单,但是因为只在像素中心附近进行采样,所以无法完全消除所有锯齿

在这里插入图片描述

引用自https://zhuanlan.zhihu.com/p/133511752

如图,红框框起的绿色像素就是MSAA会额外处理的像素部分
在这里插入图片描述

优点:

  • 相对简单且易于实现。
  • 对于线性和边缘锯齿效果较好。

缺点:

  • 对于点状和纹理锯齿效果较差。
  • 只能在像素中心附近进行采样,无法完全消除所有锯齿。
FXAA

自适应抗锯齿Fast Approximate Anti-Aliasing,一种后处理技术,原理是通过检测边缘并对其进行模糊处理来减少锯齿,它不需要额外的采样,而是在最终图像上进行处理。

优点:

  • 实现简单且计算成本低。
  • 对于大多数情况下的抗锯齿效果较好。

缺点:

  • 效果不如 MSAA 和 SSAA 高质量。
  • 可能会出现轻微的模糊效果。

阴影投射

添加阴影也是让渲染更真实的一种方案,但是之前说的环境贴图是无法投射阴影的,要实现阴影需要额外的外部光源,下面尝试给这个场景添加阴影:
在这里插入图片描述

首先,要通过给渲染器设置shadowMap.enabled来开启场景中的阴影效果:

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

renderer.shadowMap.enabled = true // 开启阴影效果
// PCFSoftShadowMap以展现更好效果的阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap

PCFSoftShadowMap的性价比较高,设置阴影时可以优先考虑
在这里插入图片描述
然后,需要对场景中能投射阴影的物体和接收阴影的物体进行设置,对于当前场景而言,接收和投射阴影的物体都是导入的模型,模型经过threejsloader解析后会转成threejs中内置的Object3DL类,所以可以通过traverse快速遍历整个模型的场景,给场景里的所有组成模型的物体设置阴影:

gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        gltf.scene.scale.set(10, 10, 10)
        scene.add(gltf.scene)
        gltf.scene.traverse(child => {
            if(child.isMesh && child.material.isMeshStandardMaterial)
                {
                    child.castShadow = true
                    child.receiveShadow = true
                }
        })
    }
)

接着,往场景中添加一个平行光源,并为其添加辅助器helpergui控制面板,方便我们调整光的投射位置、强度等属性

const directionalLight = new THREE.DirectionalLight('#ffffff', 3)
directionalLight.castShadow = true // 开启灯光的阴影投射
const helper = new THREE.DirectionalLightHelper(directionalLight)
const lightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(lightCameraHelper)
scene.add(directionalLight,helper)
gui.add(directionalLight, 'intensity').min(0).max(10).step(0.001).name('directionalLightIntensity')
gui.add(directionalLight.position, 'x').min(-10).max(10).step(0.001).name('directionalLightX')
gui.add(directionalLight.position, 'y').min(-10).max(10).step(0.001).name('directionalLightY')
gui.add(directionalLight.position, 'z').min(-10).max(10).step(0.001).name('directionalLightZ')

现在的效果:
在这里插入图片描述

现在场景中添加了两个helperDirectionalLightHelper用于标识平行光的位置(目前它被模型挡住了),THREE.CameraHelper用于标识阴影投射的方向

现在移动灯光的位置,就可以看到灯光的照射方向以及阴影的投射方向了:
在这里插入图片描述

默认情况下directionalLight是从上往下照射的,如果要修改光照的方向,按照官方文档中的说法,就得这么操作:
在这里插入图片描述

scene.add(directionalLight.target)
directionalLight.target.position.set(40,40,0)

下面是修改了灯光投射的方向后的结果
在这里插入图片描述
这里只是演示,进行下一步之前请先把修改灯光位置的代码删掉

阴影的投射也要考虑性能,通常其出发点有两个,一个是修改阴影投射摄影机的远距,使其恰好涵盖物体本身即可;一个是修改投射出的阴影质量

directionalLight.shadow.camera.far = 15 // 阴影投射摄像机的最远视野
directionalLight.shadow.mapSize.set(512, 512) // 阴影解析度

在这里插入图片描述

512 * 512的分辨率下,阴影边缘会比较模糊,把解析度改为4096,效果会好很多:

directionalLight.shadow.mapSize.set(4096, 4096) // 阴影解析度

在这里插入图片描述

最后,根据环境贴图的样子,我们需要把光调整到一个正确的位置,毕竟总不能出现“太阳在左上角,光从左下角打过来”的情况吧

比如在当前场景中,光是从天上打下来的,大概设置光的位置为

directionalLight.position.set(-4,6.5,2.5)

在这里插入图片描述

当然,过高的解析度也意味着更大的性能开销,在不同的场景下需要有不同的取舍(通常低解析度的阴影也够用了,因为用户通常不会刻意去观察这些细节)

伪影

Shadow Acne,伪影是一种在计算机图形学中常见的渲染问题,特别是在使用阴影贴图(shadow mapping)技术时。

原因:光源和物体表面之间的采样不精确导致。

导入项目中的另一个汉堡包模型,同时调整模型的缩放比例使其符合画面缩放程度:

gltfLoader.load(
    '/models/hamburger.glb',
    (gltf) =>
    {
        gltf.scene.scale.set(0.4, 0.4, 0.4) // 缩放调整
        scene.add(gltf.scene)
        gltf.scene.traverse(child => {
            if(child.isMesh && child.material.isMeshStandardMaterial)
                {
                    // 模型投射阴影
                    child.castShadow = true
                    child.receiveShadow = true
                }
        })
    }
)

乍一看似乎没什么问题:
在这里插入图片描述
但是放大看,会发现其表面出现了一些不对劲的噪点和波纹
在这里插入图片描述
当把场景亮度调低后,这些问题就更明显了:

大致的原理就是模型本身既能接收阴影,又能投射阴影,所以它的表面也会有覆盖上自己产生的阴影,因此影响了其表面的形状,产生波纹和噪点,别忘了GPU是按照像素去渲染的:
在这里插入图片描述
要解决这个问题,threejs提供了一个最简单的办法,设置directionalLight的阴影贴图偏差属性
在这里插入图片描述

directionalLight.shadow.bias = 0.0001

现在再看,波纹效果明显减少了很多,但多少还存在一些
在这里插入图片描述
并且还导致了一个新的问题,芝士的阴影也没了:
在这里插入图片描述
这是因为我们给阴影贴图移动了错误的方向,应该把bias设置为-0.0001,设置成-0.0001,芝士的阴影就回来了
在这里插入图片描述
就接下来继续解决剩下的可见的“波纹”,除了bias外,还有另一个normalBias属性可以配合一起调整,normalBias参数用于定义查询阴影贴图的位置沿物体法线方向的偏移量。增加这个值可以减少shadow acne,尤其是在光线以较小角度照射到几何体上的大场景中

使用gui控制directionalLightnormalBias属性,直到找到一个合适的值即可:

gui.add(directionalLight.shadow, 'bias').min(-0.05).max(0.05).step(0.0001)
gui.add(directionalLight.shadow, 'normalBias').min(-0.05).max(0.05).step(0.0001)

设置到0.007就差不多了:
在这里插入图片描述

另外可以看到模型的边缘有锯齿,这就要通过之前所说的抗锯齿来解决了,threejs里直接通过给renderer开启抗锯齿即可:

const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    antialias: true
})

抗锯齿ON:
在这里插入图片描述
抗锯齿OFF:
在这里插入图片描述

纹理

一个获取纹理的网址:https://polyhaven.com,不过需要注册patreon和付费才能下载,但是纹理的质量确实高,而且格式选择上足够丰富

视觉疲劳问题

长时间专注于渲染一个场景过久必然会出现视觉疲劳的问题,也许会影响你对于空间中物体的位置、颜色渲染的判断,此时最好是切换场景,去看一下看其他的有色物体来调整自己的视觉,之后再回来调整可能会发现自己原本调色不合理的地方

着色器

Web3D的范畴内来说,着色器是一种程序,由WebGL运行,需要使用GLSL语言去编写;

threeJs中,我们编写好着色器程序之后,threeJs将其传给WebGL解析,WebGL将他解析成二进制代码给GPU进行计算,最后GPU就可以将图像等渲染出来

所以,着色器是用来”绘制“(实际上是记录,然后交给GPU去绘制)几何体顶点的位置和每个像素的颜色

ThreeJs着色器的代码中,我们可以向其发送如以下信息:

  • 顶点位置
  • 几何体的变换信息(旋转、缩放等)
  • 摄像机的信息
  • 颜色、纹理、灯光、雾等

WebGL的范畴内,主要使用的是顶点着色器(Vertex Shaders)和片段着色器(Fragment Shaders

顶点着色器

通过GLSL语言,我们可以用顶点位置(position)、几何体的变换(geometry transformation)、摄像机等数据创建出顶点着色器程序,最终GPU运行这个顶点着色器,将这些东西渲染出来:

在这里插入图片描述
threejs里,Data阶段就是我们通过创建THREE.Geometry获取到的几何体的坐标、uv等数据,他们会以attributeuniforms的形式传到顶点着色器

这个意思是在GLSL语言中,把几何体的坐标等属性用attributeuniforms等关键字来表示和获取,下文的glsl语法讲解会说到

总结来说就是:顶点着色器用于处理3D模型中的每个“顶点”,可以改变顶点的位置、颜色、纹理坐标等属性。

将顶点着色器交给GPU运行后,GPU就知道了当前的几何体有哪些顶点、线、面是在画布上”可见的“,然后就可以进行下一个阶段——使用片段着色器上色了

片段着色器

Fragment Shaders,也称为像素着色器,负责计算屏幕上每个像素的颜色值,针对的是一块区域内的上色(毕竟画布是一个一个小方块组成的,最小片段应该是1px * 1px

顶点着色器的atrribute无法传给片段着色器,只能传uniforms,但是可以通过varying关键字将attribute数据传给片段着色器,如图:
在这里插入图片描述
片段着色器通过uniformsvarying获取到顶点相关的数据后,将他们进行混合(或者不混合,混合是为了实现渐变等效果);然后我们在片段着色器中声明片段的颜色,此时我们就得到了一个可用的片段着色器了

将片段着色器、顶点着色器交给WebGL编译成二进制代码传给GPU,我们的几何体就被渲染出来了

vite-plugin-glsl
https://www.npmjs.com/package/vite-plugin-glsl

npm install glslify webpack-glsl-loader --save-dev

编写着色器代码

搭建基本场景

首先搭建一个demo环境,在页面中画出一个平面几何体

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import GUI from 'lil-gui'

/**
 * Base
 */
// Debug
const gui = new GUI()

// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()

/**
 * Test mesh
 */
// Geometry
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32)

// Material
const material = new THREE.MeshBasicMaterial()

// Mesh
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.set(0.25, - 0.25, 1)
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

在这里插入图片描述

替换材质

threejs里有两个着色器材质:ShaderMaterialRawShaderMaterial,两者的区别仅在于ShaderMaterial会自动地将一些内置的attributesuniforms添加到着色器程序顶部

将代码里的材质替换为RawShaderMaterial,然后从头编写着色器代码:

// const material = THREE.MeshBasicMaterial()
const material = new THREE.RawShaderMaterial({
vertexShader: `
  uniform mat4 projectionMatrix;  // 引入投射矩阵
  uniform mat4 viewMatrix; // 引入视图矩阵
  uniform mat4 modelMatrix; // 引入模型矩阵

  attribute vec3 position; // 每个顶点的位置
  void main() {
      // 几何体的位置
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
  }
`,
fragmentShader: `
  precision mediump float; // 声明中等浮点数精度
  void main () {
    // 片段颜色
    gl_FragColor = vec4(1.0,0,0,1.0);
  }
`, 
});

至此,页面上就渲染出了一个红色的平面几何体:

在这里插入图片描述

配置开发环境

material里写模板字符串对于我们开发效率和质量都有比较大的影响,不利于我们的开发,所以我们需要类似JS、TS的代码颜色支持

VSCode就有这样的插件,名为Shader languages support for VS Code
在这里插入图片描述

着色器的代码是用glsl语言编写的,现在分别为顶点着色器和片段着色器创建两个单独的文件,并把着色器代码丢到里面,可以看到语法就有正常的高亮了:

在这里插入图片描述
这里着色器的文件后缀可以是.glsl也可以是.vs.fs等,如下表:
在这里插入图片描述
主要是glsl lint无法对.glsl后缀的文件生效,所以文件命名建议以.vs.fs命名

除了高亮外,我们还要安装代码格式化和代码检查的插件,来确保代码准确性,所以安装插件GLSL-LINT,与GLSL-CANVAS
在这里插入图片描述
然后来这里:https://github.com/KhronosGroup/glslang/releases,下载glsl linter程序:
在这里插入图片描述
找个位置解压后,打开VSCODE设置,搜索GLSL LINTvalidatorPath定位到bin目录下的validator程序即可:
在这里插入图片描述
重启vscode,以后我们就有了代码自动格式化(glsl-canvas)、代码语法检查(glsl-lint)以及着色器效果预览(glsl-canvas)了

最后,语法提示的配置,去这个地址:https://gist.github.com/lewislepton/8b17f56baa7f1790a70284e7520f9623,把第一篇json代码全部复制下来:
在这里插入图片描述
打开vscode - 文件 - 首选项 - 配置代码片段
在这里插入图片描述

搜索glsl.json,然后代码复制进去,重启vscode即可

glsl文件引入

按照目前的配置,直接引入是不行的,需要对应的文件解析器来解析对应的文件

vite环境中,需要使用vite-plugin-glsl,先安装该插件:npm install vite-plugin-glsl,然后编辑vite.config.js

import glsl from 'vite-plugin-glsl'; // 导入插件

export default {
  root: 'src/',
  publicDir: '../static/',
  base: './',
  server: {
    host: true, // Open to local network and display URL
    open: !('SANDBOX_URL' in process.env || 'CODESANDBOX_HOST' in process.env), // Open if it's not a CodeSandbox
  },
  build: {
    outDir: '../dist', // Output in the dist/ folder
    emptyOutDir: true, // Empty the folder first
    sourcemap: true, // Add sourcemap
  },
  plugins: [glsl()], // 导入插件
};

然后我们就可以引入shader文件了:

import VS from './shaders/test/vertex.vs';
import FS from './shaders/test/fragment.fs';

const material = new THREE.RawShaderMaterial({
  vertexShader: VS,
  fragmentShader: FS,
});

如果是webpack环境,需要使用raw-loader将着色器代码文件当作string引入使用,比如在umi项目中要这样配置:

// .umirc.ts
import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [{ path: '/', component: '@/pages/index' }],
  fastRefresh: {},
  chainWebpack(memo) {
    memo.module
      .rule('glsl')
      .test(/\.(glsl|vs|fs|vert|frag)$/)
      .use('raw-loader')
      .loader('raw-loader')
      .end();
  },
});

正常的webpack环境则直接配置即可:

module.exports = {
  rules: [
    {
      test: /\.(glsl|vs|fs|vert|frag)$/,
      exclude: /node_modules/,
      use: ['raw-loader']
    }
  ]
}

glsl语言

GLSL:OpenGL Shading Language,这是一种类型语言,每个变量、表达式或值都有一个明确的数据类型,并且这些类型在编译或运行时会被严格检查的语言,主要有以下特点:

  1. 无法打印,因为代码是由GPU去运行的;比如对于顶点着色器的代码而言,代码定义了每个顶点的位置计算方法,一个几何体有N个顶点需要渲染,那么GPU就会对应运行N次代码来计算出每个顶点的位置
  2. 每一行结尾必须加分号
  3. intfloat有严格的区分:如int a = 1; float b = 0.002,不能混合运算
  4. 不能缺少main函数实现,主入口必定是main函数
  5. 运算的顺序是从右往左进行的(下文会有案例演示)
  6. 这个语言没有所谓的官方文档,但有一些民间总结而来的指引网站:
    https://thebookofshaders.com/
    https://shaderific.com/glsl/common_functions.html
  7. 另外一个好用的学习openGL的英文网站:
    https://learnopengl.com/
float类型

即浮点数,和C语言中的float类似,可以进行数学运算,但必须加小数点,不能和整形数进行运算

float a = 1.0;
float b = 2.0;
float c = a * b;
float d = 1; // 报错
float f = a + 2; // 报错
int类型

即整形数据,和C语言中的int类似,声明时必须为整数,intfloat不能混用,如:

int a = 1;
float b = 0.2;
float c = a * b; -> 报错
// 计算前需要进行转换
float c = float(a) * b; // √
vec2

二维向量,由x,y组成,可以类比为threejs里的Vector2,一些要点如下:

vec2 foo = vec2(1.0, 2.0); // 基础创建方式
vec2 foo = vec2(1.0); // 等价于 vec2(1.0, 1.0)
float x = foo.x; // 获取值
foo *= 2.0; // 同时对x、y乘2.0
vec2 foo = vec2(); // 报错 不能创建空的vec2
vec3

三维向量,在x、y的基础上多一个z,类似threejsVector3,与vec2的操作类似:

vec3 foo = vec2(1.0, 2.0, 3.0); // 基础创建方式
vec3 foo = vec2(1.0); // 等价于 vec3(1.0, 1.0, 1.0)
float z = foo.z; // 获取值
foo *= 2.0; // 同时对x、y、z乘2.0
vec3 foo = vec3(); // 报错 不能创建空的vec2

除了可以使用x、y、z,还可以使用r、g、b获取值,这个对于创建一个rgb的颜色比较方便

vec3 pupleColor = vec3(0)
purpleColor.r = 0.5
purpleColor.g = 1.0

另外,可以快捷通过vec2创建一个vec3

vec2 v1 = vec2(2.0, 1.0)
vec3 v2 = vec3(v1, 3.0) // 相当于加一个z轴坐标
vec4

可以理解为四维向量,多出来的你那个记为w,也可以通过a访问:

vec4 foo = vec4(1.0,1.0,1.0,0.5);
float x = foo.x;
float y = foo.y;
float z = foo.z;
float w = foo.w;

// 或使用rgba访问:
float r = foo.r;
float g = foo.g;
float b = foo.b;
float a = foo.a;

所以vec4通常会用来声明rgba颜色(带alpha通道即"透明度"的颜色)
vec3vec2类似,vec4也可以进行类似的一些运算,这里就不做赘述

此外,向量存在一种运算成为swizzle操作,它是一种对向量的分量进行操作的运算,支持混合分量与重复分量:

vec4 color = vec4(1.0, 0.5, 0.2, 1.0);

// 提取单个分量
float red = color.r;  // 1.0
float green = color.g;  // 0.5

// 创建新的向量
vec2 rg = color.rg;  // (1.0, 0.5)
vec3 bgr = color.bgr;  // (0.2, 0.5, 1.0)
vec4 rgba = color.rgba;  // (1.0, 0.5, 0.2, 1.0)

// 重复分量
vec2 rr = color.rr;  // (1.0, 1.0)
vec3 ggg = color.ggg;  // (0.5, 0.5, 0.5)

// 混合分量
vec3 rgb = color.rgb;  // (1.0, 0.5, 0.2)
vec4 abgr = color.abgr;  // (1.0, 0.2, 0.5, 1.0)
function

即函数,类似于c语言里的函数声明:

float re() {
  float a = 1.0
  float b = 2.0
  return a+b; // 必须返回浮点数,否则报错
}

void main(){
    float result = re();
}

当然,也可以进行函数传参等操作:

float re(float a, float b) {
  return a+b
}
void main(){
    float result = re(1.0, 2.0)
    gl_Position=projectionMatrix*viewMatrix*modelMatrix*vec4(position,1.);
}

同时,其内置了很多数学函数如powmaxmin等,均与Math.powMath.max的作用一致,这里不做赘述

从GLSL代码视角理解顶点着色器

// vertex.vs
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

attribute vec3 position;
void main(){
  gl_Position=projectionMatrix*viewMatrix*modelMatrix*vec4(position,1.);
}
  1. uniformglsl语言中的关键字,用于定义一个外部传入的不可变的变量,类似于const声明

  2. mat4代表一个4x4的矩阵类型

  3. projectionMatrix代表”投影矩阵“,用于将场景从相机视图转换到标准化设备坐标系中

  4. viewMatrix代表”视图矩阵“,用于将场景从像世界坐标系转换到相机坐标系

  5. modelMatrix代表”模型矩阵“,用于将物体从局部坐标系转换到世界坐标系

  6. attribute也是glsl里的关键字,用于定义一个”属性“,每个顶点调用时position的值会自动设置为顶点的位置(以顶点的局部坐标系为准)

  7. gl_Positionglsl语言里的一个特殊内置变量,用于存储顶点着色器计算后的顶点位置,这个位置会在光栅化阶段(被片段着色器)生成片段,并最终绘制

注意,projectionMatrix、viewMatrix、modelMatrix是threeJs内部声明的变量,而不是glsl本身定义的变量,在其他3D引擎中名称不一定是这些(比如在Unity引擎中,投影矩阵命名为UNITY_MATRIX_P

所以整个顶点着色器的代码执行流程如下:

首先我们在threejs中创建GeometryShaderMaterial,使用这两个要素生成Mesh

渲染开始前,投影矩阵视图矩阵模型矩阵的值已经由threejs内部计算完成并以uniform mat4 xxxMatrix变量的形式注入完成

矩阵的计算是由threejs通过我们设置的旋转、缩放、位移计算得来的,至于计算公式就请自行查看源码了,这个并不重要

然后渲染开始,webGL根据几何体的顶点数量,告诉GPU顶点着色器代码需要执行多少次,position的值等于threejs注入的每个几何体的顶点坐标

注意,在glsl中,计算是从右往左进行的,也就是说,计算最后的顶点位置时需要:

  1. 首先将顶点的三维坐标转为四维向量,其中第四个分量为1,代表这是一个位置向量
  2. 进行模型矩阵变换,计算modelMatrix * vec4,将局部坐标系的顶点坐标转换到世界坐标系的顶点坐标。
  3. 进行视图矩阵变换,计算viewMatrix * vec4(上一步计算得来),将世界坐标系的顶点坐标转换为相机坐标系的顶点坐标
  4. 进行投影矩阵变换,计算projectionMatrix * vec4(上一步计算得来),将相机坐标系的顶点坐标转换到标准化设备坐标系的顶点坐标

所以实际上代码可以按照执行顺序改写成:

void main(){
	vec4 positionVec4 = vec4(position, 1.0);
    vec4 modelPosition=modelMatrix*positionVec4;
    vec4 viewPosition=viewMatrix*modelPosition;
    vec4 projectedPosition=projectionMatrix*viewPosition;
    gl_Position=projectedPosition;
}

然后gl_Position就会被传到片段着色器中进行下一步的上色计算

各种坐标系的讲解可以参考如下文章:
局部坐标系与全局坐标系
相机坐标系
标准化设备坐标系(NDC)
其实你不需要知道是怎么变换的,你只需要知道其中经历了这些个变换就好

从GLSL代码视角理解片段着色器

precision mediump float;
void main(){
  gl_FragColor=vec4(1.,0,0,1.);
}

片段着色器相对比较简单,gl_FragColorgl_Position一样,是内置的变量,设置的是每个片段(像素)的颜色,vec4(1.,0,0,1.)就代表纯红色,不透明

precision用于声明浮点数的精度,不同的硬件(显卡)对浮点数的处理能力不同,支持的设置有三种:

  • precision lowp float
    • 范围:[-2, 2]
      精度:至少10位有效数字
      通常用于颜色和纹理坐标,适用于大多数移动设备。
  • precision mediump float
    • 范围:[-2^14, 2^14]
      精度:至少16位有效数字
      适用于大多数颜色和纹理坐标计算,以及一些简单的数学运算。
  • precision highp float
    • 范围:[-2^62, 2^62]
      精度:至少23位有效数字
      适用于复杂的数学运算和需要高精度的场景,但可能会降低性能。

lowp通常用于移动端设备,可以有效地优化移动端的运行性能
mediump则适用于绝大多数桌面和高端移动设备
highp则适用于需要复杂的数学运算和高精度的场景,比如物理模拟、高级光照计算的场景

对于大多数WebGL应用,mediump是一个不错的选择,因为它在性能和精度之间取得了良好的平衡,优化性能时才考虑使用lowp

在GLSL里操纵几何体的变换

几何体的位置、形状等可以通过顶点着色器来修改,比如之前在顶点着色器里这样子分开每一步来写了:

void main(){
	vec4 positionVec4 = vec4(position, 1.0);
    vec4 modelPosition=modelMatrix*positionVec4;
    vec4 viewPosition=viewMatrix*modelPosition;
    vec4 projectedPosition=projectionMatrix*viewPosition;
    gl_Position=projectedPosition;
}
移动几何体

通过"模型矩阵"计算出来的modelPosition是顶点在世界坐标系中的坐标,即此时可以通过修改modelPosition的值来移动物体,比如:

    vec4 modelPosition=modelMatrix*positionVec4;
    modelPosition.z += 0.5;

就可以把物体往脸上移动0.5个距离:
在这里插入图片描述

要记得:每个顶点都会执行一次着色器代码的计算

创建波浪效果

波浪效果用三角函数可以很轻易地实现,比如对每个顶点的x坐标取正弦值赋给z坐标:(需要增大振幅效果才明显)

  vec4 modelPosition=modelMatrix*positionVec4;
  modelPosition.z=sin(modelPosition.x*20.);

就可以得到:
在这里插入图片描述
但是目前因为振幅太大导致点过于靠近,可以通过在外层缩小整体的值来让画面变缓:

  vec4 modelPosition=modelMatrix*positionVec4;
  modelPosition.z=sin(modelPosition.x*20.)*.1;

在这里插入图片描述

在glsl里,.0可以缩写为.

随机数控制

曾经我们有个操作是自己创建一个BufferGeometry并且自定义顶点坐标绘制图形,当时是通过下面的代码实现的:

const geo = new THREE.BufferGeometry()
const positions = new Float32Array(3000)
// 填充positions的每一个值
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3))

geometry里的attributes属性都会被threejs注入到着色器中,并且以同名变量的方式声明,我们可以打印geometry.atrributes查看里面的属性:

const geometry = new THREE.PlaneGeometry(1, 1, 32, 32);
console.log(geometry.attributes)

在这里插入图片描述
看到上面的图,BufferAttribute里有count属性,代表顶点的个数,即当前平面几何体的顶点个数是1089个,这就意味着顶点着色器代码至少会执行1089次;在顶点着色器中,就可以通过attribute来访问这些同名的变量,比如:

attribute vec3 position;
attribute vec2 uv;
attribute vec3 normal;

void main(){
  vec4 positionVec4=vec4(position,1.);
  vec4 modelPosition=modelMatrix*positionVec4;
  modelPosition.x+=uv.x;
  modelPosition.y+=uv.y;
  // ...
}

positionvec3的形式创建,意味着顶点着色器计算时会自动去position.array中3个3个地取值作为顶点的坐标进行计算;对于uv、normal也是同理

假设我们需要传一个自定义的值,那么就可以这样写:

const geometry = new THREE.PlaneGeometry(1, 1, 32, 32);

const count = geometry.attributes.position.count;
const randoms = new Float32Array(count);
for (let i = 0; i < count; i++) {
  randoms[i] = Math.random();
}
// 核心代码
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));

console.log(geometry.attributes)

我们创建了一个长度为顶点个数的数组,数组值用随机数填充,规定attribute里取值的步长为1,打印geomery.attributes可以看到我们自己创建的变量aRandom
在这里插入图片描述
在顶点着色器中就需要通过float进行取值,而不是vec2或vec3,这取决于attribute的步长

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
attribute float aRandom;

void main(){
  vec4 positionVec4=vec4(position,1.);
  vec4 modelPosition=modelMatrix*positionVec4;
  // modelPosition.z=sin(modelPosition.x*20.)*.1;
  modelPosition.z+=aRandom;
  vec4 viewPosition=viewMatrix*modelPosition;
  vec4 projectedPosition=projectionMatrix*viewPosition;
  gl_Position=projectedPosition;
}

这样会得到下面的结果,有点”恐怖“
在这里插入图片描述
别忘了我们可以通过比例来控制振幅:

  modelPosition.z+=aRandom * 0.1;

这样就得到还行的效果:
在这里插入图片描述

顶点着色器与片段着色器通信

threejs的两个着色器间进行数据传递有且仅有一种办法,那就是通过varying关键字实现

比如我们可以把上一步的random值传到片段着色器中控制颜色:

// vertex.vs
// ...
attribute float aRandom;
varying float vRandom;
void main(){
  // ...
  vRandom=aRandom;
  // ...
}
// fragment.fs
precision mediump float;
varying float vRandom;
void main(){
  gl_FragColor=vec4(1,vRandom,1,1.);
}

然后我们就得到了这样的效果:
在这里插入图片描述

threejs与顶点着色器通信

threejs中,ShaderMaterialRawShaderMaterial可以通过unifroms属性向顶点着色器传参,比如我们可以通过threejs来控制平面的波浪效果振幅:

const material = new THREE.RawShaderMaterial({
  vertexShader: VS,
  fragmentShader: FS,
  uniforms: {
    uFrequency: {value: 20}
  }
});
// vertex.vs
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
uniform float uFrequency;
void main(){
  vec4 positionVec4=vec4(position,1.);
  vec4 modelPosition=modelMatrix*positionVec4;
  modelPosition.z=sin(modelPosition.x*uFrequency)*.1;
  vec4 viewPosition=viewMatrix*modelPosition;
  vec4 projectedPosition=projectionMatrix*viewPosition;
  gl_Position=projectedPosition;
}

这样我们就可以在threejs修改对应的参数来进行调试或构造动画了;另外可以看到threeJs中给uFrequency设置的值是20,对于着色器而言是整型,但是却可以用float接收,因为threejs会根据需要进行对应数据的转换

除了传单一的值,还可以传THREE.Vector2,比如同时控制x、y方向的振幅就可以这样子写:

const material = new THREE.RawShaderMaterial({
  vertexShader: VS,
  fragmentShader: FS,
  uniforms: {
    uFrequency: { value: new THREE.Vector2(10, 5)}
  }
});
// vertex.vs
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
uniform vec2 uFrequency;
void main(){
  vec4 positionVec4=vec4(position,1.);
  vec4 modelPosition=modelMatrix*positionVec4;
  
  modelPosition.z=sin(modelPosition.x*uFrequency.x)*.1;
  modelPosition.z=sin(modelPosition.y*uFrequency.y)*.1;
  vec4 viewPosition=viewMatrix*modelPosition;
  vec4 projectedPosition=projectionMatrix*viewPosition;
  gl_Position=projectedPosition;
}

在这里插入图片描述

threejs与片段着色器通信

uniform创建的变量是可以同时在片段着色器、顶点着色器中通信的,所以也可以通过uniforms变量来控制平面的颜色,比如:

const material = new THREE.RawShaderMaterial({
  vertexShader: VS,
  fragmentShader: FS,
  uniforms: {
    uFrequency: { value: new THREE.Vector2(20, 10) },
    uTime: {value: 0},
    uColor: { value: new THREE.Color('cyan') }
  }
});
precision mediump float;
uniform vec3 uColor;
void main(){
  gl_FragColor=vec4(uColor,1.);
}

这里可以直接传THREE.Color,他在着色器中的数据是vec3的形式,所以除了用vec3直接生成vec4,也可以访问uColor.ruColor.guColor.b来设置颜色,这里就不赘述了

着色器加载纹理

比如我们要给平面加上国旗的纹理:
在这里插入图片描述
那么同样的,也是通过uniforms将纹理传入到着色器中:

const textureLoader = new THREE.TextureLoader();
const flagTexture = textureLoader.load('/flag-china.png')

const material = new THREE.RawShaderMaterial({
  vertexShader: VS,
  fragmentShader: FS,
  uniforms: {
    uFrequency: { value: new THREE.Vector2(10, 0) },
    uTime: {value: 0},
    uColor: { value: new THREE.Color('cyan') },
    uTexture: { value: flagTexture },
  }
});

在片段着色器中要使用sample2D这个数据类型来获取texture,并使用texture2D函数来将纹理转换成对应的vec4颜色:

// fragment.fs
precision mediump float;
uniform vec3 uColor;
varying vec2 vUV;

uniform sampler2D uTexture;
void main(){
  vec4 texutreColor=texture2D(uTexture,vUV);
  gl_FragColor=texutreColor;
  // gl_FragColor=vec4(uColor,1.);
}

sampler2Dglsl内置的数据类型,主要用于表示2D纹理,他会在着色器中访问纹理的数据,如纹理的引用、采样状态等

texture2Dglsl内置的函数,用于从2D纹理中采样颜色值,接收一个sampler2D一个二维纹理坐标作为参数,最后返回对应坐标的颜色值vec4

这里的vUV来自于顶点着色器,交互的关键代码如下:

// vertex.vs
// ...
attribute vec2 uv;
varying vec2 vUV;
void main(){
  // ...
  vUV = uv;
  // ...
}

虽然attribute不能在片段着色器中读取,但是可以通过varying将其传到片段着色器里,而arrtibute uv对应的就是几何体的uv坐标,它会被threejs自动当成attribute注入,所以可以直接通过attribute uv来读取
在这里插入图片描述
最后,就成功地将颜色换成了国旗纹理图:
在这里插入图片描述

控制动画

threeJs的逐帧渲染中,可以通过内置的Clock获取从第一帧运行开始到当前帧经过了多少秒,如:

const clock = new THREE.Clock();

const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  console.log('elapsedTime', elapsedTime);
  // Update controls
  controls.update();

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

tick();

在这里插入图片描述

所以,如果把这个值传到顶点着色器中,基本的动画就可以实现了;

创建一个uniforms变量,在每一帧的渲染函数中更新它:

const material = new THREE.RawShaderMaterial({
  vertexShader: VS,
  fragmentShader: FS,
  uniforms: {
    uFrequency: { value: new THREE.Vector2(20, 10) },
    uTime: { value: 0 }
  }
});

const clock = new THREE.Clock();

const tick = () => {
  const elapsedTime = clock.getElapsedTime();

  material.uniforms.uTime.value = elapsedTime;
  // Update controls
  controls.update();

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

tick();

在顶点着色器中用同名变量获取值,控制z方向的形变:

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
uniform vec2 uFrequency;
uniform float uTime;
void main(){
  vec4 positionVec4=vec4(position,1.);
  vec4 modelPosition=modelMatrix*positionVec4;
  modelPosition.z+=sin(modelPosition.x*uFrequency.x+uTime)*.1;
  modelPosition.z+=sin(modelPosition.y*uFrequency.y+uTime)*.1;
  
  vec4 viewPosition=viewMatrix*modelPosition;
  vec4 projectedPosition=projectionMatrix*viewPosition;
  gl_Position=projectedPosition;
}

基本的波浪动画就有了:
在这里插入图片描述

总结

  1. 顶点着色器的作用是将“顶点坐标”从“局部坐标系”转换到“标准化设备坐标系”来让GPU实现渲染

  2. threejsglsl的数据交互通过uniformsattributevarying关键字来完成,uniform可以同时被两个着色器读取,attribute只能在顶点着色器中读取,然后通过varying传入片段着色器

  3. ShaderMaterialRawShaderMateral的区别在于后者需要你自己手动声明projectionMatrixmodelMatrix等变量,所以使用后者通常可以节省一定的性能开销(因为不用初始化过多的无用变量)
    在这里插入图片描述

  4. 不要使用Date.now()来触发shaders的变化计算,因为它的值对于shader来说太大了

最后附上一个shaders实现案例的社区:https://www.shadertoy.com/

补充

理解modelMatrix

modelMatrixthreeJs内置的变换矩阵,它在默认情况下是单位矩阵

| 1 0 0 0 |
| 0 1 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |

我们在顶点着色器里,需要先经过模型矩阵变换:

vec4 modelPosition=modelMatrix*vec4(position,1.);

对于任一矩阵变换的计算公式是:

| m00 m01 m02 m03 |
| m10 m11 m12 m13 |
| m20 m21 m22 m23 |
| m30 m31 m32 m33 |

modelPosition.x = m00 * 1 + m01 * 0 + m02 * 0 + m03 * 1
modelPosition.y = m10 * 1 + m11 * 0 + m12 * 0 + m13 * 1
modelPosition.z = m20 * 1 + m21 * 0 + m22 * 0 + m23 * 1
modelPosition.w = m30 * 1 + m31 * 0 + m32 * 0 + m33 * 1

即:
modelPosition.x = m00 + m03
modelPosition.y = m10 + m13
modelPosition.z = m20 + m23
modelPosition.w = m30 + m33

所以,假如position的值为(1, 0, 1),那么计算出来的modelPosition就仍然是(1,0,1)

当我们在threejs里通过mesh.rotateX()mesh.position.x = 1mesh.scale.y = 2等操作修改了物体后,在GPU的渲染阶段,threejs会自动计算出由缩放矩阵平移矩阵旋转矩阵相乘得到的模型矩阵,以此来计算出每个顶点实际的位置

<think>好的,用户想要查找关于Three.js的学习资料或笔记,我需要根据提供的引用内容来组织回答。首先,查看用户提供的引用内容,这些引用包括Three.js的入门方法、几何体创建、模型导入、HTML结构设置以及使用npm安装库的方法。接下来,我需要将这些信息分门别类,形成结构清晰的教程笔记。 首先,应该从基础开始,比如如何引入Three.js库。引用[1]和[4]提到了在HTML文件中直接引入和使用模块化方式,可以分别作为两种不同的引入方式讲解。然后,引用[3]提到了创建几何体的例子,比如球体,可以作为一个基础示例来说明Three.js的基本用法。 接着,引用[2]提到复杂模型需要借助3D建模软件如Blender,导出gltf格式后加载,这部分可以作为模型导入的教程内容。此外,引用[5]关于dat.gui的安装和使用,可以介绍如何添加调试工具,提升开发效率。 还要注意用户可能的深层需求,比如他们可能不仅需要基础教程,还想了解进阶内容或实际应用场景。因此,在相关问题中可以涉及性能优化、动画实现、项目实战等方向。同时,需要确保引用的正确性,每个引用对应相应的内容,例如在讲几何体时引用[3],在讲模型导入时引用[2]。 另外,用户可能对学习路径和资源感兴趣,所以在教程资料推荐部分需要列出官方文档、在线课程和书籍,帮助用户系统学习。最后,生成的相关问题要覆盖用户可能遇到的后续疑问,如性能优化、与其他库结合使用等,这样可以引导他们进一步深入学习。</think>### Three.js 学习教程与资料整理 #### 1. **基础入门** - **环境搭建**: 在HTML文件中直接引入Three.js库,适用于快速入门。示例代码结构如下[^4]: ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script src="three.js"></script> <!-- 本地引入 --> </head> <body> <script type="module" src="./src/main.js"></script> <!-- 模块化引入 --> </body> </html> ``` - **几何体创建**: Three.js提供基础几何体,如球体、立方体等。例如创建球体[^3]: ```javascript var geometry = new THREE.SphereGeometry(60, 40, 40); // 半径60,经纬分段数40 ``` #### 2. **模型导入与进阶** - **复杂模型处理**: 对于复杂三维模型,建议使用Blender等建模软件导出`.gltf`格式,再通过Three.js加载[^2]。 - **工具库扩展**: 使用`dat.gui`库快速创建调试界面,安装命令[^5]: ```bash npm install --save dat.gui ``` #### 3. **动画与渲染** - **周期性渲染**: 通过`requestAnimationFrame`实现动态场景更新,例如旋转动画。 #### 4. **学习资料推荐** - **官方文档**: [Three.js官方文档](https://threejs.org/docs/) 是最权威的学习资源,涵盖API详解和示例。 - **在线课程**: - Udemy课程《Three.js入门到实战》 - B站Three.js中文教程系列 - **书籍推荐**: 《Three.js开发指南》系统讲解基础与项目实战。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值