three.js入门

前言

最近开始学Three.js了。市面上的资料并不算多,系统性的更少。有些教程照着做都是因为版本问题所以可能会卡住。一边学习一边记录一下。

推荐一下学习的资料:

  • WebGl中文网
  • Three.js零基础入门教程(郭隆邦)
  • Three.js开发指南:基于WebGL和HTML5在网页上渲染3D图形和动画(原书第3版)
  • three英文官网

什么是Three.js

它是基于WebGL封装的一个库,由于浏览器支持3D,所以可以在浏览器运行。

环境搭建

使用webpack搭建,你可以借助vscode中的live serve插件。webapck环境搭建如下:

mkdir three-demo
cd three-demo
npm init
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin three

新建public/index.html

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Learn Three.jstitle>
head>
<body>
body>
html>

新建src/index.js

import * as THREE from 'three';

let scene, camera, renrender, mesh;
let width = window.innerWidth;
let height = window.innerHeight;

const initScene = () => {
  scene = new THREE.Scene();
}

const initCamera = () => {
  camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);
  camera.position.set(200, 300, 200);
  camera.lookAt(scene.position);
}

const addObject = () => {
  let geometry = new THREE.BoxGeometry(100, 100, 100);
  let meterial = new THREE.MeshBasicMaterial();
  mesh = new THREE.Mesh(geometry, meterial);
  scene.add(mesh);
}

const initRenderer = () => {
  renrender = new THREE.WebGLRenderer();
  renrender.setSize(width, height);
  renrender.setClearColor('0xFFFFFF', 1);
  document.body.appendChild(renrender.domElement);
  renrender.render(scene, camera);
}

const init = () => {
  initScene();
  initCamera();
  addObject();
  initRenderer();
}

init();

新建webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path');

module.exports = {
  entry: './src/index.js',
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Learn Three.js',
      filename: 'index.html',
      template: 'public/index.html'
    })
  ],
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
    compress: true,
    port: 9000,
  }
}

修改package.json

"scripts": {
    "start": "webpack serve --mode development --open",
    "build": "webpack"
 },

执行npm run start,打开 http://localhost:9000/即可发现页面上有一个在旋转的正方体。

three.js入门_第1张图片

四大基本要素

在敲three.js之前,先循序渐进学习一些基础概念。这里先不阐述具体的API,后期实战的时候再补起来。

场景(Scene)

three.js需要一个舞台,这个舞台称之为Scene(场景),场景的创建特别简单:

const scene = new THREE.Scene();

照相机(Camera)

有了舞台之后,需要有一个照相机(Camera),将舞台上的内容拍摄下来传递给观众。在three.js中有多种相机类型,我们主要了透视照相机PerspectiveCamera。这种照相机比较符合现实生活,近处的物体偏大,远处的物体偏小:

const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
  • fov: 第一个参数是视角,我们人眼差不多可以看到180°的世界,这里推荐视角一般为50°
  • aspect: 渲染横向和纵向比,推荐为window.innerWidth / window.innerHeight
  • near:定义了从距离摄像机多近的距离开始渲染,为了能够从摄像机的位置看到所有物体,所以推荐值为0.1
  • far:定义了摄像机可以看到多远。不能设置太大,因为会影响性能,所以推荐值为1000

如果在绘制的过程中,你看不到你的物体,那么请检查以下,你的摄像机有没有拍摄到他们。(也可以直接翻到下面坐标系和深入相机那两节)

物体

你要渲染的内容,这里可以有多种创建方式,主要创建流程一般是这样子的:骨架 + 材料 = 物体,将物体放入场景中。

// 先用某种图像类型创建物体骨架
let geometry = new THREE.BoxGeometry(100, 100, 100);
// 用某种材料
let meterial = new THREE.MeshBasicMaterial();
// 物体 = 骨架 + 材料
let mesh = new THREE.Mesh(geometry, meterial);
// 将物体添加到场景中
scene.add(mesh);

渲染器(Renender)

渲染器的作用就是将 场景 + 摄像机 + 物体 渲染到浏览器上。渲染器有多种,但是常见的一般使用WebGLRenderer

// 创建渲染器
const renrender = new THREE.WebGLRenderer();
// 设置渲染器大小(一般就是渲染整个屏幕)
renrender.setSize(window.innerWidth, window.innerHeight);
// 设置背景颜色
renrender.setClearColor('0xFFFFFF', 1);
// 将渲染器加入到dom中
document.body.appendChild(renrender.domElement);
// 开始渲染
renrender.render(scene, camera);

小结

所以从最前面的源码中我们就不难体会了:

const init = () => {
  // 初始化场景,即创建一个舞台
  initScene();
  // 初始化摄像机,要看到舞台需要借助摄像机
  initCamera();
  // 往场景中添加物体:舞台上的表演者
  addObject();
  // 初始化渲染器,借助渲染器将其呈现在页面中
  initRenderer();
}

init();

点、线、面

在计算机世界里,3D世界是由点组成的,两个点组成一条直线,三个不在一条直线上的点就能够组成一个三角形面,无数三角形面就可以组成各种性值的物体。所以正方体一共由12个三角形面。

跟点相关的构造函数由Vector2、Vector3Vector4。不同的数值对应着是2D3D还是4D向量。(4D是啥…回头学习一下)

const addPoint = () => {
 const points = [];
 // 创建一个(-100, 0, 0)的点
 points.push(new THREE.Vector3(-100, 0, 0));
 // Vector3是Math方法,不是Object方法(详见文档分类)
 const geometry = new THREE.BufferGeometry().setFromPoints(points);
 // 使用的是点材质
 const material = new THREE.PointsMaterial({
    color: 0xff0000,
    size: 5.0 
  });
 const point = new THREE.Points(geometry, material); 
 scene.add(point);
}

three.js入门_第2张图片

点其实就是一个方向像素。

线

两点组成一条线,跟线有关的Object构造函数有Line。接下来我们来画一条白色的线。

const addLine = () => {
 const points = [];
 points.push(new THREE.Vector3(0, 0, 0));
 points.push(new THREE.Vector3(100, 0, 0));
 const geometry = new THREE.BufferGeometry().setFromPoints(points);
 // 使用的是线材质
 const material = new THREE.LineBasicMaterial({
    color: '0xffffff'
  });
 const point = new THREE.Line(geometry, material); 
 scene.add(point);
}

three.js入门_第3张图片

因为是跟着教程学的,里面提到了一个渐变色线的例子。里面的three的版本比较老,所以有问题,自己实现了一下:

const addGradientLine = () => {
  const points = [];
  points.push(new THREE.Vector3(0, 0, 0));
  points.push(new THREE.Vector3(100, 0, 0));
  var colors = new Float32Array([
    1, 0, 0, 
    0, 1, 0, 
    0, 0, 1, 
  
    1, 1, 0, 
    0, 1, 1, 
    1, 0, 1, 
  ]);
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  // 每3个就组成一个RGB颜色给一个点
  geometry.attributes.color = new THREE.BufferAttribute(colors, 3);
  const material = new THREE.LineBasicMaterial({
    // 这里是使用顶点的颜色作为线的颜色,由于两点的颜色不同,所以呈现渐变色
    vertexColors: THREE.VertexColors, 
  });
  const point = new THREE.Line(geometry, material); 
  scene.add(point);
}

three.js入门_第4张图片

文档上虽然提供了一个更改线宽度的属性,但是也有说道就是在浏览器下设置跟不设置没啥区别,考虑了性能问题。

.linewidth : Float

Controls line thickness. Default is 1.

Due to limitations of the OpenGL Core Profile with the WebGL renderer on most platforms linewidth will always be 1 regardless of the set value.

线中还有两个构造函数:

  • LineLoop: 将线头和线尾封闭起来;
  • LineSegments:两个点为一组形成一条线
const addLoopLine = () => {
  const points = [];
  points.push(new THREE.Vector3(0, 0, 0));
  points.push(new THREE.Vector3(100, 0, 0));
  points.push(new THREE.Vector3(0, 100, 0));
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({
    color: '0xffffff', 
  });
  const point = new THREE.LineLoop(geometry, material); 
  scene.add(point);
}

three.js入门_第5张图片

const addSegmentsLine = () => {
  const points = [];
  points.push(new THREE.Vector3(0, 0, 0));
  points.push(new THREE.Vector3(100, 0, 0));
  points.push(new THREE.Vector3(0, 100, 0));
  points.push(new THREE.Vector3(200, 0, 0));
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({
    color: '0xffffff', 
  });
  const point = new THREE.LineSegments(geometry, material); 
  scene.add(point);
}

three.js入门_第6张图片

如果点的个数不为偶数,则会忽略多出来的点。

其实从上面我们就不难发现了,三个点可以形成一个面。接下来完成一个作业,画一个围棋棋盘。

const addObject = () => {
  const points = [];
  points.push(new THREE.Vector3(-500, 0, 0));
  points.push(new THREE.Vector3(500, 0, 0));
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial( { color: 0xffffff, opacity: 0.2 } );
    
  for(let i = 0; i <= 20; i++){
    let line = new THREE.Line(geometry, material)
    // 分别平行移动到z轴的不同位置
    line.position.z = ( i * 50 ) - 500;
    scene.add( line );

    line = new THREE.Line( geometry, material );
    line.position.z = ( i * 50 ) - 500;
    line.rotation.y = 90 * Math.PI / 180;
    scene.add( line );
  }
}

three.js入门_第7张图片

是不是感觉上面的关于坐标的东西有点看不懂,不急,我们接下来研究一下坐标系,再来接释一下上面的坐标转换。

坐标系

three.js中含有坐标系,依靠的是右手坐标系,大拇指指向右侧,代表x轴,食指指向里面,代表y轴,中指朝上代表z轴。借助AxesHelper我们可以在Scene中绘制坐标轴。

const addAxes = () => {
  // 这里50代表坐标轴长度
  const axesHelper = new THREE.AxesHelper(50);
  scene.add(axesHelper);
}

three.js入门_第8张图片

x轴是红色,y轴代表绿色,而z轴代表蓝色。

回过头来我们看看上面那个绘制棋盘的代码。

for(let i = 0; i <= 20; i++){
    let line = new THREE.Line(geometry, material)
    // 在z轴方向上,每隔一段距离绘制一条线
    line.position.z = ( i * 50 ) - 500;
    scene.add( line );

    // ...
  }

three.js入门_第9张图片

for(let i = 0; i <= 20; i++){
    let line = new THREE.Line(geometry, material)
    // ...
    // 在x轴的方向,每隔一段数据绘制一条线
    line.position.x = ( i * 50 ) - 500;
    scene.add( line );
  }

three.js入门_第10张图片

然后我们需要把这个线绕着y轴旋转90°,这里要记住Math.PI/180就为1°,所以90°的计算方式为90 * Math.PI/180。即:

for(let i = 0; i <= 20; i++){
    let line = new THREE.Line(geometry, material)
    // ...
    line.position.x = ( i * 50 ) - 500;
    line.rotation.y = 90 * Math.PI / 180;
    scene.add( line );
  }

three.js入门_第11张图片

深入相机

学完坐标系,我们回过头来学习以下相机的问题。你是否会出现渲染出来啥都没有的情况。那肯定是你的相机设置有问题。关于相机我们要来学习一下positionlookAtup这几个经常用到的属性。

camera.position

这是设置相机的站立的位置的,默认情况下它位于原点(0,0,0),我们也可以修改:

camera.position.set(0, 100, 0);

那么此时照相机所在的位置就是在y轴上。

three.js入门_第12张图片

camera.lookAt

站好了位置,接下来要告诉照相机拍哪个方向。

// 对着场景中心点拍
camera.lookAt(scene.position);
// 直接指定拍照的点
camera.lookAt({  x : 0, y : 0, z : 0 });

camera.up

我们拍照的时候可以向上拍,想左拍,像右拍等。three.js中也有类似的方法,不过是沿着x、y、z轴拍。

当我们做如下设置的时候,坐标轴会发生变化。

camera.up.x = 0;
camera.up.y = 0,
// z轴为1,表示以z轴为相机的上方。(默认y轴为上方)
camera.up.z = 1;

three.js入门_第13张图片

我们用我们的棋盘例子来距离,默认情况下,我们是y轴向上,所以camera.up的默认值是0, 1, 0

three.js入门_第14张图片

如果我们设置为1,0,0,即x轴向上,那么:

const initCamera = () => {
  camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);
  camera.position.set(200, 300, 200);
  camera.up.x = 1;
  camera.up.y = 0;
  camera.up.z = 0;
  camera.lookAt(scene.position);
}

three.js入门_第15张图片

如果设置为0,0,1,即z轴向上啦:

const initCamera = () => {
  camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);
  camera.position.set(200, 300, 200);
  camera.up.x = 0;
  camera.up.y = 0;
  camera.up.z = 1;
  camera.lookAt(scene.position);
}

three.js入门_第16张图片

需要注意:如果camera.up不起作用,一定要看看**camera.up是否设置在camera.lookAt前**!

动画

接下来聊一聊动画。前面提过,我们需要使用渲染器,将场景和照相机通过renderer.render()显示在浏览器上面,如果我们改变了物体的位置等属性,就必须重新调用一次render函数,才可以将新的场景绘制到浏览器当中去。不然浏览器不会自动刷新场景的。

为了实现循环,我们可以使用requesetAnimationFrame。这个函数比其setIntervalsetTimeout有很多优点:

  • 由系统来决定回调函数的执行机制,根据屏幕刷新率;
  • 窗口没有激活时,动画将停止。

物体动起来有两种方式。一种是本身自己动了,一种是它相对的物体动了。

摄像机移动,所以物体往反方向移动

我们先给场景中加一个正方体:

const addObject = () => {
  let geometry = new THREE.BoxGeometry(100, 100, 100);
  let meterial = new THREE.MeshBasicMaterial();
  mesh = new THREE.Mesh(geometry, meterial);
  scene.add(mesh);
}

然后我们移动以下摄像机,往x轴方向走,这样物体看起来就是往x轴的反方向走了。

const render = () => {
  requestAnimationFrame(render);
  camera.position.x += 1;
  renrender.render(scene, camera);
}

物体自己移动

const render = () => {
  requestAnimationFrame(render);
  mesh.position.x += 1;
  renrender.render(scene, camera);
}

帧率

帧率可以理解成是屏幕刷新率。现在最佳的帧率是60fps,因为现在设备一般使用60HZ,显示器每秒刷新60次,这样页面才不会卡顿。

我们可以使用state.js来获得查看three.js中的动画性能。

npm install stats.js
import * as STATS from 'stats.js';
// ...

// 在页面左上角中显示它
const initState = () => {
  stats = new STATS();
  stats.setMode(0); // 0: fps, 1: ms
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.left = '0px';
  stats.domElement.style.top = '0px';
  document.body.appendChild( stats.domElement );
}

// 每次重新render的时候,调用更新

const render = () => {
  requestAnimationFrame(render);
  mesh.rotation.x += 0.01;
  renrender.render(scene, camera);
  // 更新,得到fps
  stats.update();
}

three.js入门_第17张图片

当帧数较低的时候,你就要注意了,可能是你的代码性能太低了造成的。一般情况下,帧数都可以跑到60的。

Tween.js

可以借助第三方库来实现动画效果。

npm i @tweenjs/tween.js

将我们的正方体每隔3s,将往x轴的反方向移动400单位。

import * as TWEEN from '@tweenjs/tween.js';

// ...

const render = () => {
  requestAnimationFrame(render);
  new TWEEN.Tween(mesh.position)
    .to({x: -400}, 3000)
    .repeat(Infinity)
    .start();
  renrender.render(scene, camera);
  stats.update();
  // 记得也要不断的更新Tween
  TWEEN.update();
}

光源

学过物理的都知道,世界上有了光,反射到我们的眼睛里,我们才看得见东西。没有光的时候我们是看不到任何物体的。在three.js中也是同理的。你可能会问,为什么前面没有涉及到光,但是渲染出来的东西还可以被看到,因为我们采用了基础材质,它与光无光,下面一节会讲到,如果使用了一些需要有光才可以看到的材质,那么就一定要设置光源了。

光源有个基类叫做Light,派生了很多种光源。我们来了解一下派生的光源。

  • THREE.AmbientLight: 环境光源,该光源的颜色将会叠加到场景现有的物体的颜色上,颜色会应用到全局,没有特别的来源方向,可以弱化阴影或者添加一些额外的颜色;
  • THREE.PointLight: 点光源,从空间的一点向所有方向发射光线,不可以投射阴影;
  • THREE.SpotLight: 聚光源,可以投射阴影;
  • THREE.DirectionalLight: 平行光,可以创建阴影,其光强并不会随着目标对象距离的增大而减弱;
  • THREE.HemisphereLight: 自然光,无法创建阴影,它考虑了天空和地面的反射;
  • THREE.AreaLight: 区域光源,可以从一个很大的区域发射光线,而不是一个点,无法创建阴影;
  • THREE.LensFlare: 这不是光源,但是可以给场景添加光晕效果。

修改前面的正方体例子,修改材质为MeshLambertMaterial

const addObject = () => {
  let geometry = new THREE.BoxGeometry(100, 100, 100);
  let meterial = new THREE.MeshLambertMaterial();
  mesh = new THREE.Mesh(geometry, meterial);
  scene.add(mesh);
}

此时是看不到的物体的,因为没有光。

three.js入门_第18张图片

如果我们加入了光之后(这里加入了环境光):

const addLight = () => {
  let light = new THREE.AmbientLight( 0xffffff );
  scene.add(light);
}

three.js入门_第19张图片

物体就看得到了。

材质

前面提到了,材质由于有了光源可以被大家伙看到了(基本材质不需要光源)。我们来介绍几种常见的材质:

  • MeshBasicMaterial: 网络基础材质,用于给几何体赋予一种简单的颜色,可以显示几何体的线框;

  • MeshLambertMaterial: 考虑光照影响的材质,用于创建暗淡的、不光亮的物体;

  • MeshPhongMaterial: 考虑光照影响的材质,用于创建光亮的物体;

  • LineBasicMaterial: 用户直线几何体,用来创建着色的直线;

  • MeshDepthMaterial: 外观的颜色深度是由物体到摄像机的距离决定的。

纹理

纹理,你可以理解成喷漆、衣服啥的。我们画一个物体,有些内容我们还是无法自己纯代码写出来的,所以可以借助设计师,让他们为我们的物体提供一个“衣服”。纹理类叫做Texture

创建纹理

THREE.Texture( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy )
  • image: 是图片路径;
  • mapping:表示的是纹理坐标;
  • wrapS:表示x轴的纹理的回环方式,就是当纹理的宽度小于需要贴图的平面的宽度的时候,平面剩下的部分应该p以何种方式贴图的问题;
  • wrapT:表示y轴的纹理回环方式;
  • magFilterminFilter表示过滤的方式;
  • format:表示加载的图片的格式,这个参数可以取值THREE.RGBAFormatRGBFormat等。THREE.RGBAFormat表示每个像素点要使用四个分量表示,分别是红、绿、蓝、透明来表示。RGBFormat则不使用透明,也就是说纹理不会有透明的效果;
  • type:表示存储纹理的内存的每一个字节的格式,是有符号,还是没有符号,是整形,还是浮点型。不过这里默认是无符号型;
  • anisotropy:各向异性过滤。使用各向异性过滤能够使纹理的效果更好,但是会消耗更多的内存、CPU、GPU时间。

纹理坐标

three.js入门_第20张图片

在正常的情况下,你在0.0到1.0的范围内指定纹理坐标。当我们用一幅图来做纹理的时候,那么这幅图就隐示的被赋予了如图一样的纹理坐标,这个纹理坐标将被对应到一个形状上。

画一个木板箱子

three.js入门_第21张图片

const addObject = () => {
  const geometry = new THREE.BoxGeometry(100, 100, 100);
  const texture = new THREE.TextureLoader().load('img/1.jpg');
  const meterial = new THREE.MeshBasicMaterial({
    map: texture
  });
  mesh = new THREE.Mesh(geometry, meterial);
  scene.add(mesh);
}

使用TextureLoader加载纹理图片,然后赋值给MeshBasicMaterialmap属性。这时候你会发现你的页面可能会出现问题。

首先图片可能会出现跨域,所以我们建议运行的时候采用服务器运行,因为我们使用的是webpack所以没有这个问题。将图片放置在public/img下,这样我们直接访问http://localhost:9000/img/1.jpg就可以直接访问到,这也是为什么上面的路径是这么写的原因。

接下来重中之重,load图片的过程是异步的,意味着你现在渲染其实是整个页面是黑的,图片还没加载完成,前面我们提到过如果物体的位置、颜色等属性发生变化的时候,应该重新渲染才对,所以我们要采用requestAnimationFrame刷新页面。

const render = () => {
  requestAnimationFrame(render);
  renrender.render(scene, camera);
}

最终例子如下:

import * as THREE from 'three';

let scene, camera, renrender, mesh;
let width = window.innerWidth;
let height = window.innerHeight;

const initScene = () => {
  scene = new THREE.Scene();
}

const initCamera = () => {
  camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);
  camera.position.set(200, 300, 200);
  camera.lookAt(scene.position);
}

const addObject = () => {
  const geometry = new THREE.BoxGeometry(100, 100, 100);
  const texture = new THREE.TextureLoader().load('img/1.jpg');
  const meterial = new THREE.MeshBasicMaterial({
    map: texture
  });
  mesh = new THREE.Mesh(geometry, meterial);
  scene.add(mesh);
}

const initRenderer = () => {
  renrender = new THREE.WebGLRenderer();
  renrender.setSize(width, height);
  document.body.appendChild(renrender.domElement);
  renrender.render(scene, camera);
}


const render = () => {
  requestAnimationFrame(render);
  renrender.render(scene, camera);
}

const init = () => {
  initScene();
  initCamera();
  addObject();
  initRenderer();
  render();
}

init();

如有错误,欢迎指出,感谢阅读~

你可能感兴趣的:(学习记录,javascript)