零基础玩转ThreeJS

1,976 阅读9分钟

去年7月扎克伯格表示,该公司正在组建一个产品团队,致力于元宇宙(Metaverse)的开发。“未来五年内,将 Facebook 从社交媒体网络转变为一个元宇宙公司。”

国内方面号称要打造全年龄段元宇宙世界的 MeteApp 公司,在 Roblox 上市后拿到了 SIG 海纳亚洲资本领投的 1 亿美元 C 轮融资

可以看到:“交互娱乐类资本瞄准的互联网未来 ”!

元宇宙概念的势头正旺,我在思考今后前端的交互不在只止步于平面,会考虑更多3D的方向。所以时候不早不晚现在开始和我一起学习ThreeJS知识吧!

image.png

基础概念篇

下图是建模软件Blender初始化项目的默认文件,图中我们可以看到在ThreeJS里最基础的组成部分。 在ThreeJs中有以下基础的概念,现在我们可以一起提前简单了解,后续跟着文章读下去会有更详细的介绍。

  • 场景Scene
  • 对象Object
    • 几何体Materials(分为点、线、面)
    • 材质Mesh
    • 纹理Texture -可以理解给物质添加上了皮肤
  • 摄像机Camera - ThreeJs中的眼睛一样的存在
  • 光Lights
  • 渲染Renderer - 渲染器把我们添加到场景里的物质、光源都渲染在页面上

你可以想象你是怎么看世界中的物体的,其实放在ThreeJS里也是同理。说着很空洞,不如一起动手做一个像这样的动态封面吧!

image.png

开始动手吧

首选我们要先在html文件里声明好canvas

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ThreeJS Starter</title>
</head>
<body>
	<!--在这定义好我们要用的canvas,后续我们要在这块画布上做很多事情~-->
    <canvas class="webgl"></canvas> 
</body>
</html>

补充一点:我想说一下我开发的习惯,我习惯是先把基础渲染环境搭好,(这个时候啥都看不到)接着放入物体(Objects),架好摄像头(Camera)、通过渲染器(Renderer)渲染在页面上,到这里基础已经搭建好了。后面只需要再按照需要开始调试交互比如点上一个光源(Lights) ,补充需要的事件(event)。所以我们就按照这个顺序继续开始编码。

1.搭建基础渲染环境创建index.js

import * as THREE from 'three'

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

/**
 * Canvas
 */
const canvas = document.querySelector('canvas.webgl')
 
/**
 * Scene
 */
const scene = new THREE.Scene()
 
/**
 * Objects
 */
const geometry = new THREE.SphereGeometry( .5, 64, 64) //我这里给了一个圆形的物质
// Materials
const material = new THREE.MeshStandardMaterial()
material.color = new THREE.Color(0x292929)
// Mesh
const sphere = new THREE.Mesh(geometry, material)
scene.add(sphere)

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

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    alpha:true
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.render(scene, camera) //将场景和摄像机渲染出来

F5刷新页面,ding~

image.png

一片空白,没有报错已经成功了~

场景里有物体、摄像头为什么还是看不到呢?

首先相机是three.js中一个很关键的要素,不同的相机呈现出来的3D效果各不相同。three中的相机分为两种。一种是正交相机和透视相机。在正交相机中,无论物体距离相机距离远或者近,呈现到用户眼睛中的物体的大小都是保持不变的。在透视相机中,符合我们用眼睛观察事物的特点,近大远小。在这里我们选择透视摄像机更符合我们想要呈现出的交互。

2.透视相机

透视相机使用PerspectiveCamera类来创建,PerspectiveCamera类的构造函数接收以下几个参数,这些参数控制着物体的展示效果。

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )

fov — 摄像机视锥体垂直视野角度也称可视角度
aspect — 摄像机视锥体长宽比
near — 摄像机距视锥体近端面的距离
far — 摄像机距视锥体远端面的距离

image.png 因为这个时候摄像头再坐标系的(0,0,0)的位置上,我们需要调动摄像头远一点,找到摄像头设置的所在行,我们来设置摄像头的position,将摄像头的z轴往后移动2位。

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.x = 0
camera.position.y = 0
camera.position.z = 2
scene.add(camera)

这个时候你应该能看到一个黑色的圆形出现在页面上了

image.png 可以说走到这里,我们已经成功了,剩下的只需要耐心的精修一下我们的对象和场景就好了。

3.引入dat.gui

这时候再引入一个新的东西 dat.gui ,我们可以利用它来可视化调试我们的代码。我们来讲它引入我们的代码里,通过调试光,我们来展示它的使用方式。

import * as dat from 'dat.gui' //在顶部添加好dat.gui

// Lights
const pointLight = new THREE.PointLight(0xffffff, 0.1)
pointLight.position.x = 2
pointLight.position.y = 3
pointLight.position.z = 4
scene.add(pointLight)

const pointLight2 = new THREE.PointLight(0xff0000, 2)
pointLight2.intensity = 5
pointLight2.position.set(-0.93, 0.95, 0.11)
scene.add(pointLight2)

const pointLight3 = new THREE.PointLight(0xe1ff, 2) //0xe1ff
pointLight3.intensity = 6.8
pointLight3.position.set(0.69, -0.8, -0.41)
scene.add(pointLight3)

//这里pointLight2可以换成pointLight3,或者pointLight。进行调试
gui.add(pointLight2.position, 'x').min(-3).max(3).step(0.01)
gui.add(pointLight2.position, 'y').min(-3).max(3).step(0.01)
gui.add(pointLight2.position, 'z').min(-3).max(3).step(0.01)
gui.add(pointLight2, 'intensity').min(0).max(10).step(0.01)

在这里我给场景添加了3个点光源,在ThreeJs中提供了多种光源比如:环境光、平行光、聚光灯、点光源、区域光。

这次我就主要介绍点光源,其他的光源如果有感兴趣的小伙伴可以在ThreeJs的文档里看到更详细的介绍。

4.点光源

由这种光源放出的光线来自同一点,且方向辐射自四面八方。

  • 蜡烛放出的光,萤火虫放出的光。
  • 手电筒的灯光,从一点发出 点光源是理想化为质点的向四面八方发出光线的光源。

点光源是抽象化了的物理概念,为了把物理问题的研究简单化。就像平时说的光滑平面,质点,无空气阻力一样,点光源在现实中也是不存在的,指的是从一个点向周围空间均匀发光的光源。

image.png

好了,介绍光源到这里,我们刷新一下页面

image.png 可以看到有很明显的光源(红色、绿色)照在我们的对象上面,并且右上角有操作条可以供我们拖动并且可以修改。但为什么改动没有效果呢?😂原因其实很简单,因为改完我们并没有重现渲染画布,所以没有效果。跟着添加一个新的方法

const tick = () => {
    // Render-注意这里把之前的render方法放入了这个函数里
  	//因为我们希望它能按照浏览器的渲染频率也帮我们进行渲染
    renderer.render(scene, camera) 
    // 在下一次渲染的时候调用tick
    window.requestAnimationFrame(tick)
}
tick()

刷新一下,在试试修改dai.gui上面的数值,看看有没有实时变化。

5.精修对象

接下来,我们一起精修精修对象理解一些关于它的相关概念吧。在ThreeJS里可以使用SphereGeometry类来创建一个球体.

SphereGeometry(radius : Float, widthSegments : Integer,
               heightSegments : Integer,
               phiStart : Float, 
               phiLength : Float, 
               thetaStart : Float, 
               thetaLength : Float)
radius — 球体半径,默认为1widthSegments — 水平分段数(沿着经线分段),最小值为3,默认值为8heightSegments — 垂直分段数(沿着纬线分段),最小值为2,默认值为6phiStart — 指定水平(经线)起始角度,默认值为0phiLength — 指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2thetaStart — 指定垂直(纬线)起始角度,默认值为0thetaLength — 指定垂直(纬线)扫描角度大小,默认值为 Math.PI

image.png

给对象添加材质

在three.js中用材质来给几何体上色,我们使用MeshStandardMaterial类,这种材质是一种机遇物理的标准材质,它不是使用近似值来表示光与表面相互作用的方式,而是使用物理上正确的模型。是一种更接近真实的渲染材质。

// Materials
const normalTexture = textureLoader.load('/NormalMap.png') //见文章末尾链接

const material = new THREE.MeshStandardMaterial()
material.metalness = 0.7
material.roughness = 0.2
material.normalMap = normalTexture
material.color = new THREE.Color(0x292929)

这里我们简单了解它的3个属性

metalness:金属性,可以理解为材料有多少像金属。木材或石头等非金属材料使用 0.0,金属使用 1.0。

roughness:粗糙度,可以理解为材料看起来有多粗糙。 0.0 表示平滑镜面反射,1.0 表示完全漫反射。

normalMap:法线贴图,法线贴图保存的不是高度信息,二十法向量的方向。简单来讲,使用法向贴图只需要使用很少的顶点和面就可以创建出细节很丰富的模型。

如图左边为设置normalMap后的效果,右边为正常效果。我们会发现设置了normalMap后的立体感非常的强烈。

image.png

我们为球体添加一张类似高尔夫球表面的贴图:

image.png

刷新一下页面看看:

image.png

我们球体加上灯光的渲染,看起来还不错。

更细致的交互

接下来我们让他转起来,并且可以随着我们的鼠标做一些不同的交互,我们一起将tick函数丰富起来。

/**
 * Animate
 */
let mouseX = 0
let mouseY = 0

let targetX = 0
let targetY = 0


const windowX = window.innerWidth / 2
const windowY = window.innerHeight / 2

function onDocumentMouseMouse(event) {
    mouseX = event.clientX - windowX
    mouseY = event.clientY - windowY
}
document.addEventListener('mousemove', onDocumentMouseMouse)

const clock = new THREE.Clock()

const tick = () => {

    targetX = mouseX * .001
    targetY = mouseY * .001

    const elapsedTime = clock.getElapsedTime()

    // // Update objects
    sphere.rotation.y = .5 * elapsedTime

    sphere.rotation.y += .5 * (targetX - sphere.rotation.y)
    sphere.rotation.x += .05 * (targetY - sphere.rotation.x)
    sphere.position.z += -.05 * (targetY - sphere.rotation.x)

   // Render-注意这里把之前的render方法放入了这个函数里
  	//因为我们希望它能按照浏览器的渲染频率也帮我们进行渲染
    renderer.render(scene, camera) 
    // 在下一次渲染的时候调用tick
    window.requestAnimationFrame(tick)
}

tick()

image.png it cool~ 在球体上面滑动时还可以看到,我们更新了小球在z轴的位置,你也可以二次开发添加上你喜欢的交互。

到此为止,我们已经完成了基础的ThreeJS的学习。

接下来我补充一些细节,让它看起来更炫。

index.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ThreeJS Starter</title>
</head>
<body>
    <div class="container">
        <h1>Feel the ThreeJS </h1>
    </div>
    <section></section>
    <canvas class="webgl"></canvas>
</body>
</html>

index.css

*
{
    margin: 0;
    padding: 0;
}

html,
body
{
    height: 100vh;
    font-family: 'Poppins';
    background-color:rgb(24,24,24) ;
}
body{
    overflow-x: hidden;
}
/* 设置图片元素与父容器背景进行混合 */
.webgl
{
    position: absolute;
    top: 0;
    left: 0;
    outline: none;
    mix-blend-mode: exclusion;
   
}

.container{
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    display: grid;
    place-items: center;
}
h1{
    font-size: 8rem;
    text-transform: uppercase;
    color: #fff;
}
section{
    height: 100vh;
}

结语

以上是最简单的ThreeJs的使用,但效果其实看起来还不错,后续我们还会引入建模的概念进来,还能做出更多更炫的交互~那今天到此结束~

image.png

学习tips:

课程学习链接

threeJs文档dat.gui

基础概念

3D坐标系

image.png

源码 可以留言我~