Three.js 从0开始

一、认识

在three.js的世界中,想渲染一个3D场景,有3个必要因素:场景(scene)、摄像机(camera)、渲染器(renderer)

场景(scene)

代表一个三维空间,用于容纳和呈现所有物质,包括物体、灯光等。场景本身深邃无物,想要呈现3D物体,必须将物体加载到 scene 中,采用右手坐标系。一切物质、要素,及其运动变化,都均处于场景中,故相当于“世界”,所有炫目繁杂的三维效果,都初始于此。

摄像机(camera)

相当于人眼,有摄像机才可以看见场景里面的一切物体和光源,连接three.js世界的通道。这个世界很大,场景下的哪一部分被取景和展示,便由此决定。three.js 内置两种摄像机。

透视相机(PerspectiveCamera)

模拟人眼所看到的景象,是渲染3D场景中使用最普遍的投影模式,此模式下,能呈现近大远小的视觉效果(透视),很符合我们人眼看事物的感觉。需要模拟现实的场景,基本都是用此相机。

正交相机(OrthographicCamera)

是一个矩形可视区域,物体只有在这个区域内才是可见的,物体无论距离摄像机是远或近,物体都会被渲染成统一比例的尺寸大小。

渲染器(renderer)

把所有的内容渲染到页面上,场景(Scene)和摄像机(Camera)都需要通过渲染器的计算,才能在此三维世界中渲染成一个二维图片显示在画布(Canvas)上

二、使用

可以使用npm以及现代构建工具来安装 three.js ,也可以用静态主机或是 CDN 来快速上手。

安装

使用 npm 模块安装 three ,可运行
npm install three -S
并在业务中引用

import * as THREE from 'three'
const scene = new THREE.Scene();

若使用 CDN 安装 three ,由于 three.js 依赖于ES module,因此任何引用它的 script 标签必须使用type="module",如下:

<script type="module">
  import * as THREE from 'https://unpkg.com/three@0.126.0/build/three.module.js';
  import { OrbitControls } from 'https://unpkg.com/three@0.126.0/examples/jsm/controls/OrbitControls.js';
  const scene = new THREE.Scene();
</script>
初始化

回顾前文,初始化3个基础元素

// 场景
const scene = new THREE.Scene();
// 摄像机
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
// 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

其中,创建摄像机使用的是PerspectiveCamera(透视摄像机)。
第一个参数是视野角度(FOV)。视野角度就是无论在什么时候,你所能在显示器上看到的场景的范围,它的单位是角度(与弧度区分开)。
第二个参数是长宽比(aspect ratio)。 也就是你用一个物体的宽除以它的高的值。需保证这个长宽比等同于挂载在 dom 中的 <canvas/> 的长宽比,否则会看到图像仿佛是被压扁的。
接下来的两个参数是近截面(near)和远截面(far),当物体某些部分比摄像机的远截面远或者比近截面近的时候,该这些部分将不会被渲染到场景中。

此外,渲染器需要设置一个渲染尺寸,使渲染器渲染出的场景填充满我们的应用程序。并将renderer(渲染器)的dom元素(renderer.domElement)添加到我们的HTML文档中,即 dom 中的 <canvas> 元素。

到这一步,完成基础搭建,由于尚未添加任何物体,此时页面空洞,呈现一片黑暗。

增加内容

在计算机里,3D世界是由点组成的,一条线由两个点组成,一个面是3个点组成,一个物体由多个3点组成的面组成。此即网格模型。
在three.js中,组成物体的网格模型(mesh)由 几何体(geometry)和材质(material)构成。

几何体(geometry)

框架中自带的物体的基础几何体,使用其构造函数,即可生成结构

材质(material)

构成物体的材料,决定了该物体的物理性质,镜面感、亮暗、颜色、透明、反光等
例如,创建一个球体:

const geometry = new THREE.SphereGeometry( 3, 32, 32 );
const material = new THREE.MeshBasicMaterial( { color: 0x215289 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

此时,物体已经添加到场景中,但是摄像机的视角是重叠在物体中心位置的,无法观察到物体本身。
修改摄像机位置,并用渲染器进行渲染

camera.position.set(10, 10, 10);

renderer.render( scene, camera );
光源

光源能是物体趋近于更真实的视觉效果。three.js 中的常见光源有
环境光:环境光充满所有的几何体表面,不产生投影;
平行光:沿着特定方向发射的光,可用来模拟太阳光的效果,可以投射阴影;
点光源:从一个点向各个方向发射的光源,可模拟一个灯泡发出的光,可以投射阴影;
聚光灯:光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大,可以投射阴影。

我们添加一个点光源:

const light = new THREE.PointLight( 0xffffff, 1, 100 );
light.position.set( 0, 5, 20 );
scene.add( light );

想要物体产生投影,还需要使用能产生投影的材料,将前面的 cube 材料进行调整

const geometry = new THREE.SphereGeometry( 3, 32, 32 );
const material = new THREE.MeshStandardMaterial( { color: 0x215289 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

还需要设置光源支持产生阴影、物体产生阴影

light.castShadow = true;
cube.castShadow = true;
cube.receiveShadow = true;
控制器

接下来,我们还需要设置控制器,能响应鼠标操作,three.js 中附加了 OrbitControls 等控制组件,引入即可

// npm
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// cdn
import { OrbitControls } from 'https://unpkg.com/three@0.126.0/examples/jsm/controls/OrbitControls.js';

// 使用
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();

鼠标的操作,需要在下一帧体现出来,因此,要在 requestAnimationFrame 中执行每一帧的渲染

function animate() {
  requestAnimationFrame( animate );
  controls.update();
  renderer.render( scene, camera );
}
animate();

此时,即可拖动、缩放场景。
代码如下:

<script type="module">
      import * as THREE from 'https://unpkg.com/three@0.126.0/build/three.module.js';
      import { OrbitControls } from 'https://unpkg.com/three@0.126.0/examples/jsm/controls/OrbitControls.js';

      // 1. 场景
      const scene = new THREE.Scene();
      // 2. 相机
      const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );
      camera.position.set(10, 10, 10);
      // 3. 渲染器
      const renderer = new THREE.WebGLRenderer({preserveDrawingBuffer: true});
      renderer.setSize( window.innerWidth, window.innerHeight );
      document.body.appendChild( renderer.domElement );
      // 4. 控制器
      const controls = new OrbitControls(camera, renderer.domElement);
      controls.update();

      const light = new THREE.PointLight( 0xffffff, 1, 100 );
      light.position.set( 0, 5, 20 );
      scene.add( light );

      const geometry = new THREE.SphereGeometry( 3, 32, 32 );
      // const material = new THREE.MeshStandardMaterial( { color: 0x215289 } );
      const texturePainting = new THREE.TextureLoader().load( 'https://img0.baidu.com/it/u=1424057713,469873542&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500')
      const material = new THREE.MeshStandardMaterial( { color: 0xffffff, map: texturePainting } );
      const cube = new THREE.Mesh( geometry, material );
      scene.add( cube );

      light.castShadow = true;
      cube.castShadow = true;
      cube.receiveShadow = true;

      function animate() {
        requestAnimationFrame( animate );
        controls.update();
        renderer.render( scene, camera );
      }
      animate();
</script>

三、案例问题

模型点击选中

光线投射Raycaster用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
原理:以点击的位置坐标为起点,向屏幕摄像机发射射线,检测射线是否与其他模型上的点相交,若相交则按照距离由近及远排列选中点,即可选取点击的该模型。

需要注意的是,在webGL中,世界坐标系是以屏幕中心为原点(0, 0, 0),且是始终不变的。面对屏幕时,右边是x正轴,上面是y正轴,屏幕向外的方向为z正轴。长度单位定义如下:窗口范围按此单位恰好是(-1,-1)到(1,1),即屏幕左下角坐标为(-1,-1),右上角坐标为(1,1)。因此,将鼠标坐标归一化为设备坐标时,x 和 y 方向的取值范围是 (-1,+1)。
参考:光线投射Raycaster
代码如下:

onChoose (event) {
  const ele = this.eleContainer  // three.js 挂载容器
  let Sx = event.clientX; // 鼠标单击位置横坐标
  let Sy = event.clientY; // 鼠标单击位置纵坐标
  let { left: offsetX, top: offsetY } = ele.getBoundingClientRect()
  // 屏幕坐标转WebGL标准设备坐标
  let x = ((Sx - offsetX) / ele.clientWidth) * 2 - 1;   // WebGL标准设备横坐标
  let y = -((Sy - offsetY ) / ele.clientHeight) * 2 + 1; // WebGL标准设备纵坐标
  // 创建一个射线投射器`Raycaster`
  let raycaster = new THREE.Raycaster();
  // 通过鼠标单击位置标准设备坐标和相机参数计算射线投射器`Raycaster`的射线属性.ray
  raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
  // 返回.intersectObjects()参数中射线选中的网格模型对象
  // 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
  let intersects = raycaster.intersectObjects([...this.meshList]);
  // 交给其他方法处理后续
  this.handleChoose(intersects?.[0]?.object)
}

按钮缩放模型

在使用OrbitControls控制器时为实现自由控制缩放,我们尝试从此控制器上着手突破,在文档 轨道控制器(OrbitControls) 中查询相关api,并无开放接口可直接控制窗口缩放,但是可以在源码中看到控制缩放的方法dollyIn
dollyInTHREE.OrbitControls 的私有函数,我们可以通过在 OrbitControls.js 中放入两个公共方法供外部使用,即可实现此需求。

this.dollyIn = function () {
    dollyIn( getZoomScale() );
    scope.update();
};
this.dollyOut = function () {
    dollyOut( getZoomScale() );
    scope.update();
};

在外部调用时,直接访问controls实例的这两个方法即可:

zoomVal > 0 ? this.controls.dollyIn() : this.controls.dollyOut();
  • end -
posted @   晨の风  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
点击右上角即可分享
微信分享提示