ThreeJs--第一个场景

121 阅读7分钟

概述

现代浏览器直接通过JavaScript就可以实现非常强大的功能。HTML5可以很容易添加语音和视频,其提供的画布可以添加具有交互功能的组件。现代浏览器开始支持WebGL,可以使用显卡资源创建高性能的二维和三维图形。WebGL实现起来有点复杂,而ThreeJs可以简化该过程。

准备工作

ThreeJs是一个JavaScript库,所以只使用一个文本编辑器就可以创建WebGL应用,这里推荐使用VSCode。

获取源码

git clone https://github.com/josdirksen/learning-threejs-third
Cloning into 'learning-threejs-third'...
remote: Enumerating objects: 1538, done.
remote: Counting objects: 100% (403/403), done.
remote: Compressing objects: 100% (100/100), done.
remote: Total 1538 (delta 309), reused 303 (delta 303), pack-reused 1135 (from 1)
Receiving objects: 100% (1538/1538), 132.11 MiB | 13.34 MiB/s, done.
Resolving deltas: 100% (797/797), done.

下载完毕后,在learning-threejs-third文件夹就可以看见所有示例。

测试示例

如果已经安装了Node,那么一定安装了NPM。使用其安装http-server模块。

npm install -g http-server

added 46 packages in 21s

15 packages are looking for funding
  run `npm fund` for details
npm notice
npm notice New minor version of npm available! 10.7.0 -> 10.8.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.8.2
npm notice To update run: npm install -g npm@10.8.2
npm notice

安装完毕后就可以直接启动http-server

http-server
Starting up http-server, serving ./

http-server version: 14.1.1

http-server settings: 
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
  http://127.0.0.1:8080
  http://192.168.68.114:8080
Hit CTRL-C to stop the server

搭建HTML框架

我们创建空的HTML框架

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>01</title>
    <!--  Threejs库 -->
    <script type="text/javascript" charset="UTF-8" src="../../learning-threejs-third/libs/three/three.js"></script>
    <!-- 控制器库 TrackballControls 可以用鼠标任意移动摄像机 -->
    <script type="text/javascript" charset="UTF-8" src="../../learning-threejs-third/libs/three/controls/TrackballControls.js"></script>
    <!-- 示例程序 -->
    <script type="text/javascript" charset="UTF-8" src="./index.js"></script>
    <link rel="stylesheet" href="../../learning-threejs-third/css/default.css">
</head>
<body>
    <!-- Threejs渲染器的输出指向这个元素 -->
    <div id="webgl-output"></div>
    <script type="text/javascript">
        (function () {
            // 调用示例程序中的init函数
            init();
        })();
    </script>
</body>
</html>
function init() {
  console.log('three 的版本是:', THREE.REVISION);
}

查看控制台会输出 three版本号

渲染并查看三维对象

创建场景并包含几个物体

对象描述
平面二维平面,可以作为场景中的地面。渲染的结果是屏幕中央的灰色地带
方块三维立方体,渲染为红色
球体三维球体,渲染为蓝色
摄像机决定你能看到的输出结果
分为x/y/z轴。确定对象三维空间中的位置。其中x轴为红色,y轴为绿色,z轴为蓝色
代码如下,只修改了index.js
function init() {
  // 定义场景
  // 场景是一个容器,用于存放所有的元素,如对象、相机和灯光
  const scene = new THREE.Scene();
  // 定义相机
  const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  // 定义渲染器
  const renderer = new THREE.WebGLRenderer();
  // 将场景的背景色设置为黑色
  renderer.setClearColor(new THREE.Color(0x000000));
  // 设置整个页面为渲染区域
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 创建坐标轴对象,bing设置轴线的粗细为20
  const axes = new THREE.AxesHelper(20);
  // 添加坐标轴对象到场景
  scene.add(axes);
  // 创建一个平面,宽度60,高度20
  const planeGeometry = new THREE.PlaneGeometry(60, 20);
  // 设置平面材质颜色
  const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xAAAAAA });
  // 将平面和材质结合成一个网格对象
  const plane = new THREE.Mesh(planeGeometry, planeMaterial);
  // 绕x轴旋转90度
  plane.rotation.x = -0.5 * Math.PI;
  // 定义位置
  plane.position.x = 15;
  // 添加到场景
  scene.add(plane);
  // 创建一个立方体
  const cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
  // 设置立方体材质
  const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xFF0000, wireframe: true });
  // 将立方体和材质结合成一个网格对象
  const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  // 设置立方体位置
  cube.position.set(-4, 3, 0);
  // 添加到场景
  scene.add(cube);
  // 创建一个球体
  const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  // 设置球体材质
  const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0x7777FF, wireframe: true });
  // 将球体和材质结合成一个网格对象
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  // 设置球体位置
  sphere.position.set(20, 4, 2);
  // 添加到场景
  scene.add(sphere);
  // 设置相机位置
  camera.position.set(-30, 40, 30);
  // 设置相机焦点
  camera.lookAt(scene.position);
  // 将渲染器的输出添加到页面
  document.getElementById("webgl-output").appendChild(renderer.domElement);
  // 渲染场景
  renderer.render(scene, camera);
}
  1. 定义场景、摄像机、渲染器对象。场景是一个容器,主要用于保存、跟踪所要渲染的物体和使用的光源。如果没有场景对象,那么无法渲染任何物体。
  2. 摄像机对象决定了能够在场景中看到什么。该对象会给予摄像机的角度来计算场景对象在浏览器中会渲染成什么样子。
  3. 创建坐标轴对象并设置轴线的粗细为20,调用scene.add方法,添加到场景中。
  4. 创建正方体、球体网格。
  5. 将渲染的结果输出到div元素中。

渲染结果

20240825-211848.jpg

添加材质、光源和阴影效果

代码如下:

function init() {
  // 定义场景
  // 场景是一个容器,用于存放所有的元素,如对象、相机和灯光
  const scene = new THREE.Scene();
  // 定义相机
  const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  // 定义渲染器
  const renderer = new THREE.WebGLRenderer();
  // 将场景的背景色设置为黑色
  renderer.setClearColor(new THREE.Color(0x000000));
  // 设置整个页面为渲染区域
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.shadowMap.enabled = true;
  // 创建坐标轴对象,bing设置轴线的粗细为20
  const axes = new THREE.AxesHelper(20);
  // 添加坐标轴对象到场景
  scene.add(axes);
  // 创建一个平面,宽度60,高度20
  const planeGeometry = new THREE.PlaneGeometry(60, 20);
  // 设置平面材质颜色
  const planeMaterial = new THREE.MeshLambertMaterial({ color: 0xAAAAAA });
  // 将平面和材质结合成一个网格对象
  const plane = new THREE.Mesh(planeGeometry, planeMaterial);
  // 绕x轴旋转90度
  plane.rotation.x = -0.5 * Math.PI;
  // 定义位置
  plane.position.x = 15;
  plane.receiveShadow = true;
  // 添加到场景
  scene.add(plane);
  // 创建一个立方体
  const cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
  // 设置立方体材质
  const cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xFF0000 });
  // 将立方体和材质结合成一个网格对象
  const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  // 设置立方体位置
  cube.position.set(-4, 3, 0);
  cube.castShadow = true;
  // 添加到场景
  scene.add(cube);
  // 创建一个球体
  const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  // 设置球体材质
  const sphereMaterial = new THREE.MeshLambertMaterial({ color: 0x7777FF });
  // 将球体和材质结合成一个网格对象
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  // 设置球体位置
  sphere.position.set(20, 4, 2);
  sphere.castShadow = true;
  // 添加到场景
  scene.add(sphere);

  // 添加光源
  const spotLight = new THREE.SpotLight(0xFFFFFF);
  // 设置光源位置
  spotLight.position.set(-40, 40, -15); 
  // 启动光源的阴影效果
  spotLight.castShadow = true;
  // 阴影的更细力度控制
  spotLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
  spotLight.shadow.camera.far = 130;
  spotLight.shadow.camera.near = 40;
  // 添加到场景
  scene.add(spotLight);
  
  // 设置相机位置
  camera.position.set(-30, 40, 30);
  // 设置相机焦点
  camera.lookAt(scene.position);
  // 将渲染器的输出添加到页面
  document.getElementById("webgl-output").appendChild(renderer.domElement);
  // 渲染场景
  renderer.render(scene, camera);
}
  1. 设置光源的属性,例如光源的位置等细粒度的控制,并添加光源到场景中
  2. THREE.MeshBasicMaterial材质不会对光源有任何反应,需要改成THREE.MeshLambertMaterial材质。
  3. 需要制定哪个物体投射阴影,哪个物体接受阴影。
plane.receiveShadow = true;
cube.castShadow = true;
sphere.castShadow = true;
  1. 设置灯光源阴影
// 启动光源的阴影效果
spotLight.castShadow = true;

显示效果

20240826-200935.jpg

场景动起来

如果想让场景动起来,首先需要解决的问题是如何在特定的时间间隔重新渲染场景。

初始化帧数统计模块

function initStats(type) {
  var panelType = (typeof type !== 'undefined' && type) && (!isNaN(type)) ? parseInt(type) : 0;
  var stats = new Stats();
  // 0: fps, 1: ms, 2: mb, 3+: custom
  stats.showPanel(panelType); 
  document.body.appendChild(stats.dom);
  return stats;
}

在初始化方法中调用

function init() {
  // 创建一个stats对象
  const stats = initStats();
  ......
}

旋转立方体

  ......
  // 渲染场景
  renderScene();

  function renderScene() {
    stats.update();
    // 旋转立方体
    cube.rotation={x:cube.rotation.x+=0.02,y:cube.rotation.y+=0.02,z:cube.rotation.z+=0.02};
    requestAnimationFrame(renderScene);
    renderer.render(scene, camera);
  }

每次调用renderScene()时使得每个坐标轴的rotation属性增加0.02,其效果就是立方体将围绕它的每个轴进行旋转。

弹跳球

......
step += 0.04;
sphere.position.x = 20 + (10 * (Math.cos(step)));
sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)));
......

只需要修改球体的位置属性,同时改变x轴和y轴位置,就可以创建出球体的运行轨迹

使用dat.GUI简化试验流程

添加用户操作界面

  • 控制小球的弹跳速度
  • 控制立方体的旋转
  ......
  // 创建一个控制对象
  let controls = new function () {
    // 设置旋转速度
    this.rotationSpeed = 0.02;
    // 设置弹跳速度
    this.bouncingSpeed = 0.03;
  };
  // 创建一个dat.GUI对象
  let gui = new dat.GUI();
  // 设置控制对象的属性 旋转速度和弹跳速度取值范围
  gui.add(controls, 'rotationSpeed', 0, 0.5);
  gui.add(controls, 'bouncingSpeed', 0, 0.5);
  renderScene();
  function renderScene() {
    stats.update();
    // 旋转立方体
    cube.rotation={x:cube.rotation.x+=controls.rotationSpeed,y:cube.rotation.y+=controls.rotationSpeed,z:cube.rotation.z+=controls.rotationSpeed};
    step += controls.bouncingSpeed;
    sphere.position.x = 20 + (10 * (Math.cos(step)));
    sphere.position.y = 2 + (10 * Math.abs(Math.sin(step)));
    requestAnimationFrame(renderScene);
    renderer.render(scene, camera);
  }
  ......
  1. 设置对象默认值
  2. 设置对象范围
  3. 将变量设置到对应的属性中

鼠标移动摄像机

  ......
  // 初始化轨道控制器
  const trackballControls = initTrackballControls(camera, renderer);
  // 创建一个时钟对象
  const clock = new THREE.Clock();
  ......

在渲染函数中更新

......
trackballControls.update(clock.getDelta());
......

场景对浏览器自适应

浏览器注册事件

let scene;
let camera;
let renderer;
window.addEventListener('resize', onresize, false);
function onresize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

总结

  • 搭建开发环境
  • 创建场景
  • 添加摄像机、光源、需要渲染的物体
  • 给场景添加阴影及动画
  • 添加辅助库dat.GUI和stats.js