邂逅Three.js探秘图形世界之美

可能了解过three.js等大型的3D 图形库同学都知道啊,学习3D技术都需要有图形学、线性代数、webgl等基础知识,以前读书学的线性代数足够扎实的话听这节课也会更容易理解,这是shader课程,希望能帮助你理解着色器,也面向第一次了解threejs的同学。

本文相关文献资料:

学习Three.js有啥用?

  • 工作上的可视化智慧小区、园区的建筑模型,包括外观、内部布局和房间分配等。

  • 元宇宙交互式导览 用户可以在元宇宙内自由探索,虚拟世界!VR和AR等技术应用。

  • 个人上的学习 Three.js 可以让你在 Web 上轻松地创建出令人惊叹的 3D 图形和交互体验,为你的项目添加更多视觉上的吸引力和创造力。

    经典案例:https://lusion.co/

让我们探秘数学的魅力,了解优雅的图形学,让枯燥无味的数字渲染出绚丽多彩的3D世界,了解底层的着色器原理是如此的精彩绝伦,今天的文章就让我们从底层开始揭秘优雅的three.js的面纱吧!

three.js的介绍和特点

Three.js 非常庞大,你可以用它做很多的事情。我们将学习所有基础知识,例如创建第一个场景、渲染、添加对象、选择正确的材料、添加纹理、为所有内容制作动画、添加光和阴影 ,甚至有些人可能会觉得这部分有点无聊,因为都是一些API的讲解。刚体(物理physic)很重要,可以看我之前发的文章

还有blender帮助我们导入导出模型和自己建模(有些偏离three的课题,但是真的很酷)

元神启动!!!!

但是篇幅不够了,所以后半段我选择给大家着重讲一讲底层的原理,大名鼎鼎的"着色器",这是大家开始觉得学习困难的地方,并且有充分的理由。着色器很难,但着色器将释放 WebGL 的真正力量

hello!three.js

铺垫了这么久,我们直接进入正题吧~!
郭隆邦安装three.js教程:
http://www.webgl3d.cn/pages/cd35b2/

安装glsl环境:

要添加语法着色,如果您使用的是 VSCode,请转到您的插件,搜索shader并安装该Shader languages support for VS Code插件。如果您使用其他代码编辑器,请寻找兼容的插件并关注流行度和评论。

three四要素

这是最简单最基础的渲染three的方式,所以我们花时间讲一下,本文还会出现动画,自适应尺寸,debug UI调试界面等看下注释就懂了。

场景

javascript 复制代码
import * as THREE from 'three'

// Scene 场景就像一个容器。我们将对象、模型、粒子、灯光等放入其中,并在某个时候要求 Three.js 渲染该场景。
const scene = new THREE.Scene()

网格

three内置有许多种几何体和材料的类型,但我们今天先简单的创建一个BoxGeometry和一个MeshBasicMaterial

javascript 复制代码
// Object 
// 形状 参数:长宽高
const geometry = new THREE.BoxGeometry(1, 1, 1)
// 材质 参数: 颜色
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
// 网格是几何体(形状)和材质的组合。
const mesh = new THREE.Mesh(geometry, material)

// 如果不向scene场景添加mesh对象,那么这个对象就无法渲染了。
scene.add(mesh)

相机

javascript 复制代码
// Sizes
const sizes = {
  width: 800,
  height: 600
}

// Camera 相机不可见。这更像是一种理论观点。当我们对场景进行渲染时,将从该摄像机的视觉角度进行渲染。(mvp会讲怎么做到的)
// 参数一:视野
// 参数二:纵横比
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
scene.add(camera)

渲染器

javascript 复制代码
// Canvas
const canvas = document.querySelector('canvas.webgl')

// ...

// Renderer 渲染器的工作是进行渲染
const renderer = new THREE.WebGLRenderer({
  canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.render(scene, camera)

一个完整的项目代码

直接看注释就懂了,用法很简单

javascript 复制代码
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as dat from 'lil-gui'

/**
 * Base
 */
// Debug
const gui = new dat.GUI({ width: 340 })

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

// Scene
// 创建场景
const scene = new THREE.Scene()

/**
 * Object
 */
// Geometry
// 创建平面几何体
const geometry = new THREE.PlaneGeometry(2, 2, 128, 128)

// 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)
// 相机默认在原点,所以要设置位置,否则就和mesh重合了
camera.position.set(1, 1, 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()

Shader 的魅力

这是最值得期待的一部分。我们上半节课已经讨论过着色器,所以大家可能会好奇它究竟是用来干什么的?

着色器是用 GLSL 编写的发送到 GPU 的程序。

看看着色器能做些什么:
https://zero.tech/
https://homunculus.jp/

Shader本身固然十分强大,但学起来也是相当有难度的。

一方面,它代码的核心就是计算,这涉及到了大量的数学和线性代数的知识,非常抽象;另一方面,它没有跟传统编程语言类似的调试工具,想要了解变量值的变化,只能通过观察画面的输出,对于初学者来说并不是很友好,这就是原生 WebGL 的学习如此困难的原因。

什么是 WebGL?

WebGL 是一种 JavaScript API,可以以惊人的速度在画布中绘制三角形 。它与大多数现代浏览器兼容,而且速度很快,因为它是直接操作使用我们的图形处理单元 (GPU)。GPU 可以进行数千次并行计算。想象一下,想要渲染一个 3D 模型,而这个模型由 900 个三角形组成------仔细想想,这并不多。每个三角形包括 3 个点。当我们想要渲染我们的模型时,GPU 将不得不计算这 2700 个点的位置。因为 GPU 可以进行并行计算,所以它会在一个原始数据中处理所有的三角形点。

GLSL语言介绍

我们简单介绍一下GLSL语言!

用于编码着色器的语言称为 GLSL ,代表 OpenGL 着色语言。很接近C语言。让我们了解其语法的基础知识。(学过c语言的有福了)

glsl 复制代码
float fooBar = 0.123; // 浮点
int foo = 123; // 整数
bool foo = true; // 布尔

// 函数
float loremIpsum()
{
  float a = 1.0;
  float b = 2.0;

  return a + b;
}

GLSL内置了很多经典的函数如也有非常实用的函数。

向量 vector

向量(也叫矢量)(vector) ,具有大小和方向的量。向量可以理解为是空间中的箭头。
基向量(basis vectors)

我们可以认为任何向量都是由2个基向量通过伸缩得到的,比如 向量v[-5,2] 可以由 基向量i[1,0] 向左伸缩5倍,基向量j[0,1]向上伸缩2倍得到

即 v = ai + bj = -5i + 2j

我们可以选择不同的基向量来获取一个合理的不同的坐标系

要注意一点,每当我们用数字描述一个向量时,都是基于基向量的
齐次坐标

vec4可以是一个齐次坐标(x, y, z,w)即为x/w, y/w, z/w),由此齐次坐标有规模不变性,还可以表示无穷远处的点。

在计算机图形学中,齐次坐标是一种扩展了传统的笛卡尔坐标系的表示方法,它包含了额外的一个分量'w'。

我们思考这样一个问题:两条平行线可以相交吗?

但是齐次坐标坐标中结果是不一样的,试想一条铁轨:

可以发现,在无穷远处,两条铁轨相交汇合为一点!

齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示。

向量加法:

向量乘法:

矩阵 matrix

线性变换(linear transformation) ,其实也可以理解成函数处理,该函数接收一个向量,经过处理后输出另一个向量。

在空间里,一个向量可以通过移动得到另一个向量

线性变换可以理解为原始的时候在xy坐标系中,是一个个正方形表格,通过线性变换后,这些表格线还是保持平行的且等距分布。

**矩阵(matrix)**代表一个特定的线性变换,矩阵跟向量的乘积就是将线性变换作用于这个向量

看视频更直观的了解一下:
https://www.bilibili.com/video/BV1ib411t7YR/?p=5&vd_source=ae1012c48d1ebdad8a46df1d056238b9

三维矩阵乘法

了解MVP变换

了解到了线性代数的一些基础知识,我们开始讲解MVP变换

MVP变换,就是M odel模型、V iew观察、P rojection投影变换三个单词的缩写。

我们先拆分一下这三个矩阵来认识了解这三个矩阵

glsl 复制代码
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

attribute vec3 position;

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

  gl_Position = projectedPosition;
}

仿射变换

https://www.bilibili.com/video/BV1254y1h7R7/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source=ae1012c48d1ebdad8a46df1d056238b9

MVP变换

我们已经了解到了仿射变换,接下来了解模型矩阵就比较好理解了看下面

模型矩阵

根据线代的知识,对于三维空间中的一个点进行平移,可以将坐标乘上一个平移矩阵,那么想让一个小盒子进行平移,则对其所有顶点都乘上一个平移矩阵,使其所有顶点都进行平移。

同理,想让一个小盒子进行大小的放缩,让其顶点都成上一个放缩矩阵即可。

除了平移和放缩,变换还包括旋转,在三维空间中,绕哪个轴进行旋转,都有不同的公式。具体的公式由极坐标 即可较容易推导出。具体推导过程以及绕Y轴旋转的特殊性可以看:

将上述三中类型的矩阵作用在一起,即可得到模型变换矩阵,要注意**矩阵的顺序是从右到左作用到局部空间中的顶点上的。**即先进行缩放、旋转后,再进行平移。

后面的视图矩阵、投影矩阵不是我们本节课的重点,所以不会过多讲解了,感兴趣可以自行去了解,这两个矩阵也更复杂。可以参考课程GAMES101-现代计算机图形学入门-闫令琪https://www.bilibili.com/video/BV1X7411F744/?spm_id_from=333.999.0.0&vd_source=ae1012c48d1ebdad8a46df1d056238b9

让我们开始创建第一个shader吧!

顶点着色器

创建vertex.glsl

glsl 复制代码
uniform mat4 projectionMatrix; //透视矩阵
uniform mat4 viewMatrix; // 视图矩阵
uniform mat4 modelMatrix; // 模型矩阵

attribute vec3 position; // 传入的顶点坐标

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

我们最重要的就是要知道这三个变量具体的含义projectionMatrix * viewMatrix * modelMatrix (mvp变换)

片段着色器

片段着色器的目的是为几何体的每个可见片段着色。

创建fragment.glsl

glsl 复制代码
void main(){ 
    gl_FragColor = vec4(0.5, 0.8, 1.0, 1.0); 
}

我们只要操作顶点着色器和片段着色器即可,不用理会图元和光栅化。

javascript 复制代码
//首先替换我们的material
const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader
})

创建着色器glsl文件并且导入

javascript 复制代码
import testVertexShader from './shaders/test/vertex.glsl'
import testFragmentShader from './shaders/test/fragment.glsl'

这时候我们可以看到页面上出现了一个蓝色的平面了。

入门课程就到这里结束了,这些基础知识希望可以帮助你在学习threejs的过程中走的更远!

下集分享 - shader实操和shader-toy上的案例。

相关推荐
菜鸡且互啄6943 分钟前
在线教育平台,easyexcel使用案例
java·开发语言
电饭叔2 小时前
《python程序语言设计》2018版第5章第52题利用turtle绘制sin函数
开发语言·python
weixin_452600692 小时前
如何为老化的汽车铅酸电池充电
开发语言·单片机·安全·汽车·电机·电源模块·充电桩
Java资深爱好者3 小时前
如何在std::map中查找元素
开发语言·c++
YCCX_XFF213 小时前
ImportError: DLL load failed while importing _imaging: 操作系统无法运行 %1
开发语言·python
哥廷根数学学派4 小时前
基于Maximin的异常检测方法(MATLAB)
开发语言·人工智能·深度学习·机器学习
杰哥在此5 小时前
Java面试题:讨论持续集成/持续部署的重要性,并描述如何在项目中实施CI/CD流程
java·开发语言·python·面试·编程
Unity打怪升级5 小时前
Laravel: 优雅构建PHP应用的现代框架
开发语言·php·laravel
强迫老板HelloWord5 小时前
前端JS特效第22波:jQuery滑动手风琴内容切换特效
前端·javascript·jquery
C.C5 小时前
java IO流(1)
java·开发语言