场景、相机和渲染器

Three.js整个系统主要包含场景Scene、相机Camera和WebGL渲染器WebGLRenderer三大块,其中场景又包含模型和光源。WebGL渲染器的主要作用就是把相机对应场景渲染出来,显示在网页Cnavas画布上。

three.js点滴yan(整理后)-LMLPHP
three.js点滴yan(整理后)-LMLPHP

Three.js源码

Three.js各个构造函数对应的源码位于three.js-master中src文件中,three.js-master\build目录下的three.js文件是通过src目录下的所有代码块打包而来。

想了解学习3D引擎Three.js整个系统的原理,自然需要Three.js引擎three.js-master\src目录下的所有源码。

JavaScript和WebGL基础

Three.js是通过JavaScript语言对WebGL API和着色器代码封装后得到的3D引擎,阅读Three.js源码自然需要有一定的JavaScript和WebGL基础。

了解Three.js每个API对应构造函数是如何封装,构造函数的参数是如何设置属性的,对象的属性和方法是如何封装的,对象的方法是如何改变对象属性的,类与类之间的继承关系,这些都需要你对JavaScript的语言有很深的了解。

three.js-master\src\renderers目录下都是与WebGL渲染器WebGLRenderer相关的代码块,WebGL渲染器代码主要对WebGL API和着色器代码进行了封装。
three.js-master\src\renderers\shaders目录下ShaderChunk和ShaderLib文件主要是一系列具有特定功能的着色器GLSL代码块。
开发者编程常用的WebGL渲染器对应的源码就是renderers目录下WebGLRenderer.js文件,该文件会调用renderers目录下shader和webgl两个文件中的代码。

类封装案例

比如Mesh.js文件部分源码

// 引入Mesh.js依赖的js文件
...
import { Vector3 } from '../math/Vector3.js';
import { Matrix4 } from '../math/Matrix4.js';
import { Object3D } from '../core/Object3D.js';
...
// 声明一个构造函数Mesh
function Mesh( geometry, material ) {
  // 通过call方法,Meshlz可以批量继承Object3D类的方法和属性
  // 网格模型`Mesh`的基类是`Object3D`
	Object3D.call( this );

	this.type = 'Mesh';

  // 如果构造函数没有参数,系统设置属性默认
  // 如果系统有参数,直接把参数设置为属性对应的属性值
  this.geometry = geometry !== undefined ? geometry : new BufferGeometry();
	this.material = material !== undefined ? material : new MeshBasicMaterial( { color: Math.random() * 0xffffff } );

}

源码阅读技巧

  • 编辑器查找关键词,比如查找src目录下的那个.js文件封装了WebGL APIgl.drawArrays()
    比如查找一个对象的方法方法.cross()

  • src目录下源码和three.js文档目录是一一对应关系,比如src目录下math文件中都是Three.js文档Math分类下API对应的源码文件。

渲染器.render()方法和WebGL APIgl.drawArrays()

three.js点滴yan(整理后)-LMLPHP

如果你有原生WebGL基础,阅读Threejs源码将会很容易理解一些对象的方法和属性,比如
Three.js渲染器的渲染方法.render(),执行该方法就相当于执行WebGL 绘制方法gl.drawArrays()

在原生WebGL代码中,执行绘制方法gl.drawArrays()就会在Cnvas画布上绘制一帧图片,自然Threejs的渲染方法.render()同理,周期性执行
绘制方法gl.drawArrays()可以更新帧缓冲区数据,也就是更新Canvas画布显示图像,.render()方法同理可以实现一样的效果。

// 渲染函数
function render() {
  renderer.render(scene, camera); //执行渲染操作
  mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
  requestAnimationFrame(render);//请求再次执行渲染函数render,渲染下一帧
}
render();

WebGLBufferRenderer.js文件封装了原生WebGL方法gl.drawArrays()

// WebGLBufferRenderer.js文件源码
// 提供了一个render方法,在WebGLRenderer.js中会调用
function WebGLBufferRenderer( gl, extensions, info ) {
  ...
	function render( start, count ) {
    // 封装了原生WebGL API:gl.drawArrays()
		gl.drawArrays( mode, start, count );
		info.update( count, mode );
	}
  ...
  this.render = render;
}  

WebGL渲染器源码文件WebGLRenderer.js中的渲染方法.render()调用了WebGLBufferRenderer.js文件封装的原生WebGL方法gl.drawArrays()

gl.drawArrays——>WebGLBufferRenderer.js——>renderBufferDirect——>renderObject——>renderObjects——>render

import {WebGLBufferRenderer} from './webgl/WebGLBufferRenderer.js';
bufferRenderer = new WebGLBufferRenderer(_gl, extensions, info);
var renderer = bufferRenderer;
this.renderBufferDirect = function(camera, fog, geometry, material, object, group) {
...
renderer.render(drawStart, drawCount);
...
}
function renderObject(object, scene, camera, geometry, material, group) {
_this.renderBufferDirect(camera, scene.fog, geometry, material, object, group);
}
function renderObjects(renderList, scene, camera...) {
  ...
  renderObject(object, scene, camera, geometry, material, group);
  ...
}
this.render = function(scene, camera...) {
  ...
  renderObjects(opaqueObjects, scene, camera);
  ...
}

WebGL渲染器WebGLRenderer

想要了解Three.js系统的原理,简单点说就是了解WebGL渲染器渲染解析场景和相机的原理,如果想深入了解渲染器的渲染原理,不是短时间内能够做到的,本节课主要目的是假设你有一定的原生WebGL基础,然后对Three.js的渲染器进行简单介绍,后面的课程会进行深入的介绍。

.domElement属性

如果想通过WebGL渲染一个三维场景,需要HTML的Canvas画布元素实现,通过渲染器构造函数WebGLRenderer创建一个渲染器对象 ,如果构造函数参数没有设置canvas对象,系统会自动创建一个Cnavas元素。

通过canvas元素返回WebGL上下文gl对象才能调用相关的WebGL API

//通过getElementById()方法获取canvas画布
var canvas=document.getElementById('webgl');
//通过方法getContext()获取WebGL上下文
var gl=canvas.getContext('webgl');
...
gl.enableVertexAttribArray(aposLocation);
...
gl.drawArrays(gl.LINE_LOOP,0,4);
...

WebGLRenderer.js源码对.domElement属性的相关封装

  function WebGLRenderer(parameters) {
    ...
    parameters = parameters || {};
    //如果canvas画布没有通过参数对象parameters的canvas属性设置,通过API创建一个
    var _canvas = parameters.canvas !== undefined ? parameters.canvas : document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'),
    ...
    把_canvas赋值给WebGL渲染器对象的domElement属性
    this.domElement = _canvas;
  }

通过渲染器.domElement属性,可以把Three.js的canvas画布插入到html任何一个元素中,可以在整个body页面上全局显示,也可以插入一个div元素中局部显示,注意canvas画布尺寸设置。

  // 创建渲染器对象
  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  //body元素中插入Threejs创建的包含canvas对象
  document.body.appendChild(renderer.domElement);

.setSize(width, height)方法

WebGLRenderer.js源码对.setSize()方法的相关封装

...
// pixelRatio:像素比率
_pixelRatio = 1,
...
this.setSize = function(width, height, updateStyle) {
...
  _canvas.width = width * _pixelRatio;
  _canvas.height = height * _pixelRatio;

  if (updateStyle !== false) {
    _canvas.style.width = width + 'px';
    _canvas.style.height = height + 'px';
  }
...
};

全屏设置,也就是canvas画布宽高度和窗口尺寸一样

  var width = window.innerWidth; //窗口宽度
  var height = window.innerHeight; //窗口高度

  var renderer = new THREE.WebGLRenderer();
  // 设置渲染尺寸,本质就是设置canvas元素宽高度
  renderer.setSize(width, height);

局部渲染,通过.setSize()方法设置canvas画布的宽高度像素

  var width = window.innerWidth; //窗口宽度
  var height = window.innerHeight; //窗口高度

  var renderer = new THREE.WebGLRenderer();
  // 设置渲染尺寸,本质就是设置canvas元素宽高度
  renderer.setSize(300, 300);

帧缓冲区的相关封装

学习Three.js与帧缓冲区相关的封装,首先要了解WebGL中帧缓冲区的概念,帧缓冲区包含颜色缓冲区、深度缓冲区、模板缓冲区,颜色缓冲区存储片元的颜色数据,也就是像素数据,深度缓冲存储片元的深度数据,用于WebGL渲染流程中的深度测试环节,被遮挡的片元会被剔除,不会显示在canvas画布上。

渲染器方法.clear(color,depth,stencil)

原生WebGL方法gl.clear()用来清除帧缓冲区数据

// 清除颜色缓冲区数据
gl.clear(gl.COLOR_BUFFER_BIT)
// 清除深度缓冲区数据
gl.clear(gl.DEPTH_BUFFER_BIT)
// 清除模板缓冲区数据
gl.clear(gl.STENCIL_BUFFER_BIT)
// 清除帧缓冲区的颜色、深度和模板缓冲中数据
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);

WebGLRenderer.js源码对渲染器方法.clear()的封装

this.clear = function(color, depth, stencil) {
  // “&” 和 “|” 是位运算操作符
  var bits = 0;

  if (color === undefined || color) bits |= _gl.COLOR_BUFFER_BIT;
  if (depth === undefined || depth) bits |= _gl.DEPTH_BUFFER_BIT;
  if (stencil === undefined || stencil) bits |= _gl.STENCIL_BUFFER_BIT;

  _gl.clear(bits);

};

渲染器方法.clearDepth()

清除帧缓冲区中深度缓冲区中的片元深度数据

renderer.clearDepth()
// WebGLRenderer.js源码
this.clearDepth = function() {
  this.clear(false, true, false);
};

渲染器属性.autoClear

Three.js渲染器默认情况下,本次执行render方法之前,会把上次执行render方法后帧缓冲区中的数据清除

autoClear默认值true,设置为false,执行render方法的时候不会自动清除上次渲染帧缓冲区中的数据

renderer.autoClear = false;

renderers目录下渲染器相关代码块介绍

前面说过想了解Threejs的渲染器是如何解析场景和相机对象然后进行渲染的,除了需要了解场景和相机对象的结构外,还需要阅读渲染器文件WebGLRenderer.js源码来了解渲染解析过程。

this.render开始阅读

渲染器渲染器的时候会把场景和相机作为参数然后调用.render()方法,所以想了解Threejs渲染器的解析过程,自然需要在WebGLRenderer.js文件中查找到.render()方法封装的源码,可以通过查找关键词this.render找到。

renderer.render(scene, camera);

由于Threejs渲染器是很复杂的,render函数中会调用其它经过多层封装的函数,自然第一次阅读render函数的源码,基本大多数代码都不是很好理解。

材质Material和着色器shader——shaders目录

Three.js渲染器解析场景中网格模型材质Material的时候,会调用材质对象对应的顶点着色器和片元着色器代码,然后经过编译处理后在GPU上执行。

着色器代码文件目录是系three.js-master\src\renderers\shaders,shaders目录下有两个着色器代码的文件ShaderChunk和ShaderLib。

ShaderChunk目录下的着色器代码文件.glsl都是具有特定功能的模块,ShaderLib目录下的着色器文件会通过#include <ShaderChunk中着色器文件名>调用ShaderChunk目录下特定功能的着色器代码块构建出来具有具有特定功能的顶点着色器文件和片元着色器文件。

  • 点材质PointsMaterial:顶点着色器文件points_vert.glsl、片元着色器文件points_frag.glsl
  • 基础网格材质MeshBasicMaterial:顶点着色器文件meshbasic_vert.glsl、片元着色器文件meshbasic_frag.glsl
  • 高光网格材质MeshPhongMaterial:顶点着色器文件meshphong_vert.glsl、片元着色器文件meshphong_frag.glsl

ShaderChunk.js:用来获得ShaderChunk和ShaderLib文件中的着色器代码

ShaderLib.js:设置好点、线、网格材质对应的uniforms变量值、顶点着色器代码、片元着色器代码

UniformsLib.js、UniformsUtils.js:着色器中uniform变量相关的对象

调用webgl目录下函数

WebGLRenderer.js会通过importfrom关键字引入webgl目录下的函数,然后通过new关键字实例化返回一个对象,webgl目录下文件中封装的函数创建对象的方式有多种,比如工厂模式、构造函数模式、原型模式…

  • 构造函数模式:比如WebGLTextures.js…
  • 工厂模式:比如WebGLGeometries.js、WebGLObjects、WebGLAttributes…

webgl目录下封装的函数创建的对象具有一些方法和属性,用于完成特定的功能。

- WebGLGeometries.js:解析几何体对象Geometry或BufferGeometry
- WebGLLights:解析光源对象
- WebGLAttributes.js:解析BufferAttribute对象,也就是说从BufferAttribute对象提取顶点数据,把顶点数据传入创建的顶点缓冲区
- WebGLShader.js:创建着色器对象
- WebGLProgram.js:创建程序对象
- WebGLUniforms.js: unifomr变量传值相关
- WebGLInfo.js:记录渲染器的渲染信息,比如渲染了多少帧frame
...

webgl渲染器代码模块的方法和属性

以WebGLInfo.js为例分析,WebGLInfo.js是一个工厂函数,通过new关键词实例化该函数返回一个具有特定属性和方法的对象。

function WebGLInfo( gl ) {
	var memory = {...};
	var render = {frame: 0,calls: 0,triangles: 0,...};
  // update函数
	function update( count, mode, instanceCount ) {...}
  // reset函数
	function reset() {...}

	return {
    // WebGLInfo对象属性:memory、render、programs、autoReset
		memory: memory,
		render: render,
		programs: null,
		autoReset: true,
    // WebGLInfo对象方法:update、reset
		reset: reset,
		update: update
	};

}

渲染器文件WebGLRenderer.js中调用WebGLInfo对象的方法和属性

import {WebGLInfo} from './webgl/WebGLInfo.js';
...
info = new WebGLInfo(_gl);
...
_this.info = info;
...
if (this.info.autoReset) this.info.reset();

判断对象类型的属性

Threejs渲染器解析Threejs对象的时候需要判断它是什么类型,所以通过Threejs构造函数创建对象的时候会初始化设置一些属性用于判断对象的类型,比如type属性,获得对象构造函数的名字,isMesh、isLine、isGroup等属性用来判断一个对象是那种对象,以便于Threejs渲染器进行归类处理。


Three.js封装WebGL顶点数据

如果已有一定的WebGL基础,自然都有顶点数据的概念,比如顶点位置、顶点法向量、顶点UV坐标、顶点颜色等等。
如果直接使用WebGL编写程序肯定比较麻烦,所以对WebGL进行一定的封装,这样的话对于开发者肯定会更有好,因此这节课来讲解Threejs是如何对象对WebGL中顶点数据进行封装和组织。

  • 相比很多Geometry和BufferGeometry有什么区别?本节课也可以回答你。

原生WebGL代码

/**
 创建顶点位置数据数组data,Javascript中小数点前面的0可以省略
 **/
var data=new Float32Array([
    .5,.5,.5,-.5,.5,.5,-.5,-.5,.5,.5,.5,.5,-.5,-.5,.5,.5,-.5,.5,      //面1
    .5,.5,.5,.5,-.5,.5,.5,-.5,-.5,.5,.5,.5,.5,-.5,-.5,.5,.5,-.5,      //面2
    .5,.5,.5,.5,.5,-.5,-.5,.5,-.5,.5,.5,.5,-.5,.5,-.5,-.5,.5,.5,      //面3
    -.5,.5,.5,-.5,.5,-.5,-.5,-.5,-.5,-.5,.5,.5,-.5,-.5,-.5,-.5,-.5,.5,//面4
    -.5,-.5,-.5,.5,-.5,-.5,.5,-.5,.5,-.5,-.5,-.5,.5,-.5,.5,-.5,-.5,.5,//面5
    .5,-.5,-.5,-.5,-.5,-.5,-.5,.5,-.5,.5,-.5,-.5,-.5,.5,-.5,.5,.5,-.5 //面6
]);
/**
 创建顶点颜色数组colorData
 **/
var colorData = new Float32Array([
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面1
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面2
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面3
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面4
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面5
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0 //红色——面6
]);
/**
 *顶点法向量数组normalData
 **/
var normalData = new Float32Array([
    0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,//z轴正方向——面1
    1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,//x轴正方向——面2
    0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,//y轴正方向——面3
    -1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,//x轴负方向——面4
    0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,//y轴负方向——面5
    0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1//z轴负方向——面6
]);

BufferAttribute对象

Three.js通过BufferAttribute对象对顶点数据进行了封装,该对象.array属性的值就是一个存储了一系列顶点数据的数组。
BufferAttribute对象可以表示一组顶点位置数据,可以表示一组顶点法向量数据,可以表示一组顶点颜色数据…

//类型数组创建顶点数据
var vertices = new Float32Array([
  0, 0, 0, //顶点1坐标
  50, 0, 0, //顶点2坐标
  0, 100, 0, //顶点3坐标
  0, 0, 10, //顶点4坐标
  0, 0, 100, //顶点5坐标
  50, 0, 10, //顶点6坐标
]);
// 创建属性缓冲区对象表示一组顶点坐标
// 参数3表示类型化数组vertices中的顶点数据3个为一组,表示一个顶点的xyz坐标
var attribue = new THREE.BufferAttribute(vertices, 3);
console.log('查看顶点数据',attribue.array);
console.log('attribue.itemSize',attribue.itemSize);

可以通过构造函数BufferAttribute参数设置.array.itemSize属性,也可以直接设置这两个属性的值。

var attribue = new THREE.BufferAttribute();
// 设置顶点数据
attribue.array = vertices;
// 设置多少个元素为一组,与WebGL的一个方法gl.vertexAttribPointer()第二个参数相关。
// 比如UV坐标只有xy,需要设置itemSize=2,比如顶点坐标只设置xy,z使用系统默认值,也可以只设置itemSize=2
attribue.itemSize = 3;

Three.js构造函数BufferAttribute的名字由Buffer和attribute两个单词组成,顶点数据需要传入WebGL APIgl.createBuffer()创建顶点缓冲区中,这是BufferAttribute类名Buffer部分的来源,顶点数据在着色器代码中对应了attribute变量,比如attribute vec4 position;声明了一个顶点位置变量,对应缓冲区中的顶点位置数据;attribute vec4 normal;声明了一个顶点法向量变量,对应顶点缓冲区中的顶点颜色数据,这是BufferAttribute类名Attribute部分的来源,BufferAttribute对象可以翻译为顶点缓冲属性对象。

缓冲几何体对象BufferGeometry

一个几何体对象BufferGeometry可以理解为所有种类顶点数据的一个集合,BufferGeometry对象.attributes的属性包含了所有顶点数据,通过BufferGeometry.attributes.position设置或返回顶点位置数据,通过BufferGeometry.attributes.uv设置或返回顶点UV坐标数据,通过BufferGeometry.attributes.normal设置或返回顶点法向量数据等等。.attributes.position.attributes.uv.attributes.normal的属性值都是包含顶点数据的BufferAttribute对象

var geometry = new THREE.BufferGeometry(); //声明一个缓冲几何体对象

//类型数组创建顶点位置position数据
var vertices = new Float32Array([
  0, 0, 0, //顶点1坐标
  50, 0, 0, //顶点2坐标
  0, 100, 0, //顶点3坐标

  0, 0, 10, //顶点4坐标
  0, 0, 100, //顶点5坐标
  50, 0, 10, //顶点6坐标
]);
// 创建属性缓冲区对象
var attribue = new THREE.BufferAttribute(vertices, 3); //3个为一组,作为一个顶点的xyz坐标
// 设置几何体attributes属性的位置position属性
geometry.attributes.position = attribue;

//类型数组创建顶点颜色color数据
var colors = new Float32Array([
  1, 0, 0, //顶点1颜色
  0, 1, 0, //顶点2颜色
  0, 0, 1, //顶点3颜色

  1, 1, 0, //顶点4颜色
  0, 1, 1, //顶点5颜色
  1, 0, 1, //顶点6颜色
]);
// 设置几何体attributes属性的颜色color属性
//3个为一组,表示一个顶点的颜色数据RGB
geometry.attributes.color = new THREE.BufferAttribute(colors, 3);

创建特定几何形状的缓冲类型API的基类都是BufferGeometry,比如BoxBufferGeometry,这些API本质上都是通过一定的算法自动化生成各种几何形状对应的顶点数据。

//创建一个缓冲区类型立方体
var geometry = new THREE.BoxBufferGeometry(100, 100,100);
console.log('几何体所有顶点数据',geometry.attributes);
console.log('几何体顶点位置数据',geometry.attributes.position);
console.log('几何体顶点法向量数据',geometry.attributes.normal);
console.log('几何体顶点索引数据',geometry.index);

BufferGeometry的顶点索引属性.index

如果你了解WebGL的顶点索引绘制肯定对顶点索引数据并不陌生,这样的话自然更容易理解该属性,BufferGeometry.index的属性值和.attributes.position一样都是包含顶点数据的BufferAttribute对象。

    //  顶点索引数组
    var indexes = new Uint8Array([0,1,2,3,4,5,6,7,0,4,1,5,2,6,3,7]);
    //创建缓冲区对象
    var indexesBuffer=gl.createBuffer();
    //绑定缓冲区对象
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexesBuffer);
    //索引数组indexes数据传入缓冲区
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indexes,gl.STATIC_DRAW);

Geometry和BufferGeometry区别

几何体对象Geometry和缓冲几何体对象BufferGeometry表达的信息是一样的,都是对几何体顶点数据的封装,只是数据结构不同,也就是属性不同。GeometryBufferGeometry可以相互转化,Three.js渲染器解析几何体,会从几何体对象提取顶点数据传入WebGL顶点缓冲区,如果解析的是BufferGeometry对象,直接访问.attributes属性提取顶点数据就可以,如果几何体Geometry对象,Three.js渲染器会先把Geometry转化BufferGeometry对象,然后提取顶点数据,关于Three.js是如何解析几何体传入顶点缓冲区,然后与顶点着色器中的attribute变量绑定,在后面的课程会详细介绍Threejs渲染器是如何解析几何体的,本节课主要是讲解Threejs是如何封装顶点数据的,不详细讲解Three.js如何解析顶点封装后的得到的几何体对象。

Geometry数据结构
 //创建一个立方体几何对象Geometry
var geometry = new THREE.BoxGeometry(100, 100, 100);
console.log(geometry);
console.log('几何体顶点位置数据',geometry.vertices);
console.log('三角面数据',geometry.faces);

解析几何体提取顶点数据

上节课讲解了如何把WebGL的顶点数据封装为Three.js的几何体对象,这节课就来讲解Three.js的渲染器在渲染的时候,如何解析几何体对象,提取顶点数据,然后调用WebGL API创建顶点缓冲区,并把创提取的顶点数据传入创建的顶点缓冲区。

本章节的内容是给大家讲解Three.js渲染器是如何解析场景和渲染器对象的,本节课讲解解析的一个环节,也就是Threejs如何解析几何体并创建相应的顶点缓冲区。

three.js点滴yan(整理后)-LMLPHP

原生WebGL

原生WebGL通过gl.createBuffer()创建一个顶点缓冲区对象,用来存储顶点位置、顶点颜色、顶点法向量等数据。如果你理解了这一段代码,自然就很容易理解Three.js的几何体对象和相应的缓冲区。

// 顶点位置数据
var data=new Float32Array([0.5,0.5,0.3...]);
 // 创建缓冲区buffer,传入顶点位置数据data
var buffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);
gl.vertexAttribPointer(aposLocation,3,gl.FLOAT,false,0,0);

整体解析过程简介

Three.js渲染器解析几何体对象,会从几何体对象提取顶点数据传入WebGL顶点缓冲区的时候,如果解析的是BufferGeometry对象,直接访问.attributes属性提取顶点数据就可以,比如获得顶点位置数据,通过.attributes.position.array获得顶点数据。如果Three.js渲染器解析的几何体是Geometry对象,会先把Geometry对象转化为BufferGeometry对象然后再解析。
three.js点滴yan(整理后)-LMLPHP

顶点颜色属性Geometry.colors

点模型Points、线模型Line对象与网格模型Mesh对象的几何体结构Geometry略有不同。

Geometry.colors属性包含的顶点颜色数据在点模型Points、线模型Line中使用,几何体对象的该属性在网格模型Mesh中不起作用。 网格模型Mesh使用几何体对象三角面Face3的顶点颜色属性Face3.vertexColors设置颜色。

Geometry转化为BufferGeometry

通过BufferGeometry.setFromObject(object)方法可以把参数可以把object模型对象的几何体geometry转化为BufferGeometry,点模型Points和线模型Line使用一套解析转化规则,网格模型Mesh使用一种转化规则。

对于网格模型的几何体Geometry转化为BufferGeometry的时候,需要先把Geometry对象转化为直接几何体对象DirectGeometry,然后再转化为BufferGeometry对象。

相关函数

WebGLAttributes.js、WebGLGeometries.js和WebGLObjects.js是工厂函数,执行这三个函数都会返回一个具有特定方法的对象,WebGLObjects.js会调用WebGLGeometries对象的方法,WebGLGeometries.js会调用WebGLAttributes对象的方法。

three.js点滴yan(整理后)-LMLPHP

WebGLAttributes.js

.update(BufferAttribute)方法

解析BufferAttribute对象,也就是说从BufferAttribute对象的.array属性提取顶点数据,把顶点数据传入WebGL顶点缓冲区,对gl.createBuffer()gl.、bufferData()等WebGL API进行了封装。

WebGLGeometries.js

.get()方法

参数:.get(Object,Object.geometry)

如果Object.geometry是BufferGeometry直接返回,如果是Geometry,会转化为BufferGeometry,点线模型和网格模型的Geometry转化规则不同,所以参数需要传入Object,代码需要判断Object是Points和Line还是Mesh。

.update(BufferGeometry)方法

通过BufferGeometry的.index.attributes属性,获得包含顶点数据的BufferAttribute对象,然后BufferAttribute作为参数调用WebGLAttributes.update()方法,提取顶点数据并传入顶点缓冲区。

WebGLObjects.js

.update(Object)方法

从模型对象Object提取几何体数据,也就是模型的几何体属性 Object.geometry,然后调用WebGLGeometries.get()方法,并把Object和Object.geometry作为参数,直接get方法后返回BufferGeometry,然后调用WebGLGeometries.update()方法,把BufferGeometry作为参数。

WebGLRenderer.js

场景中遍历获得的对象object,如果是Points、Line或Mesh模型,调用WebGLObjects.update()方法,并把object作为参数。

WebGLRenderer.js

import {WebGLAttributes} from './webgl/WebGLAttributes.js';
import {WebGLGeometries} from './webgl/WebGLGeometries.js';
import {WebGLObjects} from './webgl/WebGLObjects.js';

var attributes, geometries, objects;

attributes = new WebGLAttributes(_gl);
// WebGLAttributes作为WebGLGeometries参数
geometries = new WebGLGeometries(_gl, attributes, info);
// WebGLGeometries作为WebGLObjects参数
objects = new WebGLObjects(geometries, info);


function projectObject(object, camera, sortObjects) {
	...
	else if (object.isMesh || object.isLine || object.isPoints) {
		var geometry = objects.update(object);
	}
	...
  // 递归算法:遍历对象
	var children = object.children;
	for (var i = 0, l = children.length; i < l; i++) {
		projectObject(children[i], camera, sortObjects);
	}
}
// 渲染方法中调用projectObject
this.render = function(scene, camera, renderTarget, forceClear) {
	...
	// 递归遍历场景对象,对于其中的点、线和网格模型需要解析它们的几何体,提取顶点数据,并传入顶点缓冲区
	projectObject(scene, camera, _this.sortObjects);
	...
}

Threejs层级模型的封装和解析

通过前面课程的学习你对Threejs的层级模型应该有一定的认识,具体说也就是根节点场景对象Scene和它的子孙对象构成的树结构。

基类Object3D

基类Object3D封装了用于构建树结构的方法.add()和属性.children

继承

场景对象Scene、组对象Group基类是Object3DObject3D也是网格Mesh、点Points、线Line等模型对象的基类。

可能有些学员会思考组对象GroupObject3D有什么区别,有些时候会混合使用,其实看一下源码就能明白,Group的基类是Object3D,基本没有扩展增加属性和方法,功能上没有什么区别,主要是Group更语义化,看到名字就知道什么意思。

网格模型对象作为场景对象的子对象

var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

点光源添加到场景中

var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300);
scene.add(point);

网格模型插入组对象,组对象插入场景中。

var leftEyeMesh = new THREE.Mesh();
var rightEyeMesh = new THREE.Mesh();

var headGroup = new THREE.Group();
headGroup.add(leftEyeMesh, rightEyeMesh);

scene.add(headGroup);
console.log('查看场景子对象',scene.children);
console.log('查看headGroup的子对象',headGroup.children);

递归算法

如果像你想通过阅读WebGLRenderer.js源码,了解Threejs渲染器是如何解析遍历场景对象Scene的所有子孙对象的,需要了解一下递归算法,如果你有数据结构和算法方面的基础,自然很熟悉,如果没有相关的基础最后学习了解一下。

树结构

  • 树结构节点名字:基类Object3D.name属性给节点命名
  • 递归遍历树结构:递归遍历方法.traverse( callback )
  • 查找树节点:.getObjectByName(name)递归遍历对象的子对象返回与参数设置名字一致的对象

WebGL渲染器函数projectObject

WebGLRenderer.js源码,在render函数中会调用projectObject函数

function projectObject(object, camera, sortObjects) {
...
// 封装了递归算法,可以用来遍历树结构对象,比如场景对象Scene
  var children = object.children;
  // 递归算法遍历场景对象
  for (var i = 0, l = children.length; i < l; i++) {
    projectObject(children[i], camera, sortObjects);
  }
}

WebGLRenderer.js源码,projectObject函数会递归遍历场景对象进行分类

this.render=function(){
  ...
  // 遍历场景对象,并对场景对象中的节点进行分类,比如光源对象,比如模型对象
  projectObject(scene, camera, _this.sortObjects);
  ...
}

projectObject函数对象递归遍历到的子对象节点进行分类处理,然后WebGL渲染器在对象分类好的不同对象进行渲染器解析,关于进一步的渲染解析过程这里不讲解。

// currentRenderList对象用于存储点、线和和网格模型的对象
var currentRenderList = renderLists = new WebGLRenderLists();
currentRenderList = renderLists.get(scene, camera);
// currentRenderList对象用于存储光源、点精灵等对象
var currentRenderStaterenderStates = new WebGLRenderStates();
currentRenderState = renderStates.get(scene, camera);

// 对象Scene的后代节点对象进行分类处理
function projectObject(object, camera, sortObjects) {
...
  if (visible) {
    // 判断对象是不是光源对象,是的话插入WebGL渲染状态对象的state属性中
    if (object.isLight) {
      //光源信息插入到currentRenderState对象的.state.lightsArray属性中
      currentRenderState.pushLight(object);
    }
    // 判断对象是不是精灵对象
    else if (object.isSprite) {
    //光源信息插入到currentRenderState对象的.state.spritesArray属性中
        currentRenderState.pushSprite(object);
    }
     else if (object.isMesh || object.isLine || object.isPoints) {
      // 把模型对象相关信息批量存储到currentRenderList对象
      currentRenderList.push(object, geometry, material, _vector3.z, null);
    }

  }
}

本地矩阵.materix和世界矩阵.matrixWorld

一个对象的本地矩阵.materix包含了该对象的旋转、平移和缩放变换,本地矩阵是平移矩阵、缩放矩阵和旋转矩阵的乘积。

一个对象的世界矩阵.matrixWorld是该对象本地矩阵及其所有所有祖宗对象本地矩阵的乘积,或者每一个对象的世界矩阵是对象本地矩阵和父对象的世界矩阵的乘积。

Object3D

Object3D是网格Mesh、点Points、线Line等模型对象的基类,组对象Group也是Object3D对象的基类。

Object3D封装本地矩阵.matrix、位置.position、缩放.scale、角度.rotation等属性,封装了旋转相关方法.rotateX().rotateZ(),平移相关方法.translateX().translateZ()

四元数属性.quaternion和角度属性.rotation

两个属性表达的含义是一样的,都是旋转相关的信息,都会被转化为旋转矩阵。

方法改变属性

对于Three.js一些对象的属性可以直接设置属性值,也可以通过方法改变属性值。

执行旋转方法.rotateZ()查看,查看角度属性.rotation属性值欧拉对象z属性的变化

// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 绕z轴旋转
mesh.rotateZ(Math.PI)
console.log('查看角度属性值的变化',mesh.rotation);
console.log('查看四元数属性变化',mesh.quaternion);

执行平移方法.translateX()查看,查看位置.position属性值x分量变化

// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 沿着x轴平移100
mesh.translateX(100)
console.log('查看位置属性的变化',mesh.position);

更新本地矩阵属性.updateMatrix()

执行Object3D.updateMatrix()方法可以提取位置.position、缩放.scale和四元数.quaternion的属性值转化为变换矩阵设置本地矩阵属性.matrix

// Object3D.js源码
updateMatrix: function () {
  // 把位置、四元数、缩放属性转化为平移、旋转、缩放矩阵,三个矩阵的乘积是本地矩阵
  this.matrix.compose( this.position, this.quaternion, this.scale );
  this.matrixWorldNeedsUpdate = true;
},

// Matrix4.js源码
// 通过属性值设置变换矩阵
compose: function ( position, quaternion, scale ) {
  // 四元数矩阵转化为旋转矩阵,然后改变本地矩阵
  this.makeRotationFromQuaternion( quaternion );
  // 缩放属性转化为缩放矩阵,然后改变本地矩阵
  this.scale( scale );
  // 位置属性转化为平移矩阵,然后改变本地矩阵
  this.setPosition( position );
  return this;
},

// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 缩放网格模型
mesh.scale.set(900,900,900)
// 位置、角度、缩放属性值更新到矩阵属性matrix
mesh.updateMatrix();
console.log('查看本地矩阵属性matrix',mesh.matrix.elements);

更新世界矩阵属性.updateMatrixWorld()

调用.updateMatrixWorld()方法首先会更新对象的本地矩阵属性,然后更新对象的世界矩阵属性。

.updateMatrixWorld()方法封装了递归算法,会遍历对象的所有子对象,对象本身和

// Object3D.js源码
updateMatrixWorld: function ( force ) {
  // 更新对象的本地矩阵属性
  if ( this.matrixAutoUpdate ) this.updateMatrix();
  ...
  if ( this.parent === null ) {
    // 如果一个对象没有父对象,也就是树结构对象的根节点对象,世界矩阵就等于本地矩阵
    this.matrixWorld.copy( this.matrix );

  } else {
    // 更新对象的世界矩阵,父对象的世界矩阵和对象本地矩阵的乘积
    this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
  }
  ...
 // 通过递归算法遍历一个对象的所有子对象,执行与根对象一样的操作更新本地和世界矩阵属性
  var children = this.children;
  for ( var i = 0, l = children.length; i < l; i ++ ) {
    children[ i ].updateMatrixWorld( force );
  }
},

WebGL渲染器

场景对象的.autoUpdate属性默认值是true,执行.render()方法的时候scene.updateMatrixWorld()默认执行,也就是说执行
Threejs执行渲染器渲染方法的时候,场景对象所有子孙对象的世界矩阵属性和本地矩阵属性才会更新。

// WebGLRenderer.js源码
this.render=function(){
  ...
  // WebGL渲染器中执行场景对象的updateMatrixWorld()方法,更新场景和场景所有子孙对象的本地矩阵
  if (scene.autoUpdate === true) scene.updateMatrixWorld();
  ...
}

世界坐标和本地坐标

位置属性.position表示本地坐标,也就是说该对象相对父对象的偏移位置。通过Object3D.getWorldPosition()方法可以返回一个模型的世界坐标,是模型对象相对坐标原点的位置坐标,也就是该对象位置属性.position及其所有祖宗对象的.position相加。

var worldPosition = new THREE.Vector3();
mesh.getWorldPosition(worldPosition)
console.log('世界坐标',worldPosition);
console.log('本地坐标',mesh.position);
// Object3D.js源码
getWorldPosition: function ( target ) {
  if ( target === undefined ) {
    console.warn( 'THREE.Object3D: .getWorldPosition() target is now required' );
    target = new Vector3();
  }
  this.updateMatrixWorld( true );
  通过矩阵对象setFromMatrixPosition方法从世界矩阵中提取平移矩阵分量,然后转化为position属性
  return target.setFromMatrixPosition( this.matrixWorld );
},

场景子对象具体的分类结果

Three.js渲染器执行渲染方法.render()时候会遍历场景对象,然后对场景对象的后代进行分类,然后把同类的对象进行集中存储,然后再渲染分类好的对象。

学习本节课需要简单阅读下WebGLRenderLists.js和WebGLRenderStates.js源码文件,渲染器文件WebGLRenderer.js封装的projectObject函数和.render()方法

WebGLRenderLists.js

执行WebGLRenderLists对象的.get()方法会返回一个对象,返回对象具有.opaque.transparent属性,.push.init.sort方法。

.opaque.transparent属性的值是数组对象,用来存储场景下所有的点线网格模型。

.push方法,执行该方法会把模型对象相关的信息插入到.opaque.transparent属性值数组中

// WebGLRenderLists.js源码
// object:点Points、线Line或网格Mesh模型对象
// geometry:WebGLObjects.js的封装的`.update()`方法返回的`BufferGeometry`
// material:模型对象的材质属性值,比如点材质、高光网格材质等等
function push( object, geometry, material, z, group ) {
  var opaque = [];
  var transparent = [];
  ...
  // 一个模型对象的相关信息构成的对象作为.opaque属性或.transparent属性的元素
  renderItem = {
    id: object.id,
    object: object,
    geometry: geometry,
    material: material,
    program: material.program,
    renderOrder: object.renderOrder,
    z: z,
    group: group
  };
  ...
  // 判断材质是否开启透明,如果开启,则把renderItem对象归类到透明数组transparent中
  // 如果transparent是flase,则把renderItem对象插入到不透明数组opaque中
  // transparent === false执行opaque.push( renderItem )
  // transparent === true执行transparent.push( renderItem )
  ( material.transparent === true ? transparent : opaque ).push( renderItem );
}
// WebGLRenderer.js源码
var renderLists = new WebGLRenderLists();
currentRenderList = renderLists.get(scene, camera);

// 调用对象的方法
function projectObject(object, camera, sortObjects) {
  ...
  else if (object.isMesh || object.isLine || object.isPoints) {
    // 把模型对象相关信息批量存储到currentRenderList对象
    currentRenderList.push(object, geometry, material, _vector3.z, null);
  }
  ...
}

// 访问对象属性中的数据
this.render=function(){
  // material.transparent=false,对象材质未开启透明
  var opaqueObjects = currentRenderList.opaque;
  // material.transparent=true,对象材质开启透明
  var transparentObjects = currentRenderList.transparent;
}

WebGLRenderStates.js

执行WebGLRenderStates对象的.get()方法会返回一个对象,返回对象具有.state属性,.pushLight.pushSprite等方法。

.state属性类似.opaque.transparent属性用来存储场景中的部分对象,.state.lightsArray属性存储所有光源对象,.state.spritesArray属性存储所有精灵模型对象。

.pushLight().pushSprite()等方法类似WebGLRenderLists的.push方法,.pushLight()方法用于光源对象归类,把参数光源对象存储到.state.lightsArray属性数组中,.pushSprite()方法用于精灵模型对象归类,参数精灵模型对象存储到.state.spritesArray属性数组中,

// WebGLRenderStates.js源码
	var lightsArray = [];
	var spritesArray = [];

// 插入光源数组函数
	function pushLight( light ) {
		lightsArray.push( light );
	}
// 精灵
	function pushSprite( shadowLight ) {
		spritesArray.push( shadowLight );
	}
  // 存储光源对象、精灵模型对象....
	var state = {
		lightsArray: lightsArray,
		shadowsArray: shadowsArray,
		spritesArray: spritesArray,
	};
  // 返回值
	return {
		state: state,
		pushLight: pushLight,
		pushSprite: pushSprite
	};
// WebGLRenderer.js源码
var renderStates = new WebGLRenderStates();
currentRenderState = renderStates.get(scene, camera);

// 调用对象的方法
function projectObject(object, camera, sortObjects) {
  ...
  if (object.isLight) {
    //光源信息插入到currentRenderState对象的.state.lightsArray属性中
    currentRenderState.pushLight(object);
  }
  // 判断对象是不是精灵对象
  else if (object.isSprite) {
  //光源信息插入到currentRenderState对象的.state.spritesArray属性中
      currentRenderState.pushSprite(object);
  }
  ...
}

// 访问对象属性中的数据
this.render=function(){
  var spritesArray = currentRenderState.state.spritesArray;
}

function initMaterial(material, fog, object) {
  var lights = currentRenderState.state.lights;
  var shadowsArray = currentRenderState.state.shadowsArray;
}

projectObject函数

阅读WebGLRenderer.js源码封装的projectObject函数了解分类过程。

projectObject函数对象递归遍历到的子对象节点进行分类处理,然后WebGL渲染器在对象分类好的不同对象进行渲染器解析,关于进一步的渲染解析过程这里不讲解。

// 对象Scene的后代节点对象进行分类处理
function projectObject(object, camera, sortObjects) {
...
  if (visible) {
    // 判断对象是不是光源对象,是的话插入WebGL渲染状态对象的state属性中
    if (object.isLight) {
      //光源信息插入到currentRenderState对象的.state.lightsArray属性中
      currentRenderState.pushLight(object);
    }
    // 判断对象是不是精灵对象
    else if (object.isSprite) {
    //光源信息插入到currentRenderState对象的.state.spritesArray属性中
        currentRenderState.pushSprite(object);
    }
     else if (object.isMesh || object.isLine || object.isPoints) {
      // 把模型对象相关信息批量存储到currentRenderList对象
      currentRenderList.push(object, geometry, material, _vector3.z, null);
    }

  }
}

WebGLObjects的.update()方法

执行projectObject函数会调用WebGLObjects.js的封装的.update()方法,调用改变方法会解析点线面网格模型对象的几何体Geometry数据,如果是Geometry会转化为BufferGeometry,然后从BufferGeometry提取顶点数据,并传入创建的顶点缓冲区,更多详细内容参考本章第5节“5.解析几何体提取顶点数据”。

执行WebGLObjects.js的封装的.update()方法的返回值是BufferGeometry.

执行完push方法,currentRenderList.transparent或currentRenderList.opaque数组元素的geometry属性值是BufferGeometry。

// 对象Scene的后代节点对象进行分类处理
function projectObject(object, camera, sortObjects) {
  var geometry = objects.update(object);
  var material = object.material;
      // 把模型对象相关信息批量存储到currentRenderList对象
  currentRenderList.push(object, geometry, material, _vector3.z, null);
}

WebGLRenderer.js的.render()方法

WebGLRenderer.js渲染方法.render(),先调用projectObject函数对场景的后代对象进行分类,然后调用renderObjects函数渲染场景中所有的点、线、网格模型对象。

gl.drawArrays——>WebGLBufferRenderer.js——>renderBufferDirect——>renderObject——>renderObjects——>render

// WebGLRenderer.js源码
// currentRenderList对象用于存储点、线和和网格模型的对象
var renderLists = new WebGLRenderLists();
currentRenderList = renderLists.get(scene, camera);
// currentRenderList对象用于存储光源、点精灵等对象
var renderStates = new WebGLRenderStates();
currentRenderState = renderStates.get(scene, camera);

this.render=function(){

  // 场景的后代对象分类
  ...
  // 遍历场景对象,并对场景对象中的节点进行分类,比如光源对象,比如模型对象
  projectObject(scene, camera, _this.sortObjects);
  ...

  // 渲染模型对象

  // material.transparent=false,对象材质未开启透明
  var opaqueObjects = currentRenderList.opaque;
  // material.transparent=true,对象材质开启透明
  var transparentObjects = currentRenderList.transparent;
  // 渲染材质未开启透明的模型对象集合
  renderObjects(opaqueObjects, scene, camera);
  // 渲染材质开启透明的模型对象集合
  renderObjects(transparentObjects, scene, camera);
}


点线网格模型和绘制模式

Three.js点Points、线Line、网格Mesh模型都有几何体geometry数据,对于这些不同类别的模型对象Threejs渲染的时候调用WebGL绘制函数gl.drawArrays()gl.drawElements()的时候系统设置的绘制模式不同。

学习本节课的内容需要先简单阅读以下源码:

  • WebGLRenderer.js源码封装的的.renderBufferDirect()方法
  • WebGLBufferRenderer.js源码
  • WebGLIndexedBufferRenderer.js源码

WebGLRenderer.js

WebGLRenderer.js封装了WebGL绘制函数gl.drawArrays( mode, start, count );

.setMode()方法,设置gl.drawArrays();的绘制模式mode。

.render()方法,封装了WebGL APIgl.drawArrays();

WebGLIndexedBufferRenderer.js

WebGLIndexedBufferRenderer.js封装了WebGL绘制函数gl.drawElements();

.setMode()方法,设置gl.drawElements();的绘制模式mode。

.render()方法,封装了WebGL APIgl.drawElements();

.renderBufferDirect()方法

执行.renderBufferDirect()方法会调用WebGL绘制函数gl.drawArrays()gl.drawElements()绘制顶点数据,渲染点Points、线Line、网格Mesh模型的时候,在执行绘制函数之前需要根据模型的类别设置绘制函数的绘制模式mode。

import {WebGLBufferRenderer} from './webgl/WebGLBufferRenderer.js';
import {WebGLIndexedBufferRenderer} from './webgl/WebGLIndexedBufferRenderer.js';
bufferRenderer = new WebGLBufferRenderer(_gl, extensions, info);
indexedBufferRenderer = new WebGLIndexedBufferRenderer(_gl, extensions, info);
this.renderBufferDirect = function(camera, fog, geometry, material, object, group) {
...

  var renderer = bufferRenderer;

  // 如果存在顶点索引数据,把渲染器设置为WebGLIndexedBufferRenderer
  if (index !== null) {
  attribute = attributes.get(index);
  renderer = indexedBufferRenderer;
  renderer.setIndex(attribute);
  }
  ...
  if (object.isMesh) {
      // wireframe默认false
      if (material.wireframe === true) {
        // 开启材质线框显示效果,使用线绘制模式gl.LINES
        state.setLineWidth(material.wireframeLinewidth * getTargetPixelRatio());
        renderer.setMode(_gl.LINES);
      } else {
        // 网格模型对象具有drawMode属性,默认值为TrianglesDrawMode
        switch (object.drawMode) {
          case TrianglesDrawMode:
            renderer.setMode(_gl.TRIANGLES);
            break;
          case TriangleStripDrawMode:
            renderer.setMode(_gl.TRIANGLE_STRIP);
            break;
          case TriangleFanDrawMode:
            renderer.setMode(_gl.TRIANGLE_FAN);
            break;
        }
      }
    } else if (object.isLine) {
      var lineWidth = material.linewidth;
      if (lineWidth === undefined) lineWidth = 1; // Not using Line*Material
      state.setLineWidth(lineWidth * getTargetPixelRatio());      
      if (object.isLineSegments) {
        // LineSegments模型对象
        renderer.setMode(_gl.LINES);
      } else if (object.isLineLoop) {
        // LineLoop模型对象
        renderer.setMode(_gl.LINE_LOOP);
      } else {
        // Line模型对象
        renderer.setMode(_gl.LINE_STRIP);
      }
    } else if (object.isPoints) {
     // 点模型对象Points
      renderer.setMode(_gl.POINTS);
    }else if (object.isPoints) {
      // 点模型对象使用点绘制模式
      renderer.setMode(_gl.POINTS);

    }
    ...

    // 设置好绘制模式后,调用WebGLRenderer.js封装的render函数,相当于执行`gl.drawArrays();`
    renderer.render(drawStart, drawCount);
}

光源对象分类

THree.js渲染函数会通过调用projectObject函数递归遍历场景对象,然后对场景对象的后代进行分类,这种光源全部划分为一类,存入currentRenderState.state.lightsArray属性中,作为属性值数组的元素。

projectObject函数

阅读WebGLRenderer.js源码封装的projectObject函数了解分类过程,递归遍历场景,所有object.isLight=true的对象全部归类为currentRenderState.state.lightsArray属性中。

// 对象Scene的后代节点对象进行分类处理
function projectObject(object, camera, sortObjects) {
  // 判断对象是不是光源对象,是的话插入WebGL渲染状态对象的state属性中
  if (object.isLight) {
    //光源信息插入到currentRenderState对象的.state.lightsArray属性中
    currentRenderState.pushLight(object);
  }
}

光源对象封装

点光源PointLight、平行光DirectionalLight、环境光AmbientLight等所有种类光源对象的基类都是Light,点光源PointLight具有isPointLight属性,平行光DirectionalLight具有isDirectionalLight属性,环境光AmbientLight具有isAmbientLight属性,这些光源对象也会继承基类Light的属性isLight

var PointLight = new THREE.PointLight()
// 点光源PointLight属性isPointLight默认值true
console.log(PointLight.isPointLight);
// 点光源PointLight继承基类Light的isLight属性
console.log(PointLight.isLight);

WebGLLights.js

WebGLLights.js文件封装了光源分类的具体函数和属性。new实例化WebGLLights工厂函数返回一个对象具有.set()方法和.state属性,.state
具有一些属性对应光源对象的分类。

WebGLRenderState.js

import { WebGLLights } from './WebGLLights.js';

function WebGLRenderState() {
...
var lights = new WebGLLights();

function setupLights( camera ) {
  // 调用WebGLLights对象的set方法对已经分类的光源对象进行二次分类
  lights.setup( lightsArray, shadowsArray, camera );

}

var state = {
  // 存储所有光源对象的数组
  lightsArray: lightsArray,
  shadowsArray: shadowsArray,
  // 存储所有精灵模型对象的数据
  spritesArray: spritesArray,
  // 属性值是WebGLLights对象,具有state属性,state属性又具有directional、point等属性,用来分别存储方向光、点光源等类型光源
  lights: lights
};
...  
}

WebGLRenderer.js

调用.setupLights方法对象光源进行二次分类,分类结果存储到currentRenderState.state.lights属性


this.render=function(){

  projectObject(scene, camera, _this.sortObjects);

  currentRenderState.setupLights(camera);
}

function initMaterial(material, fog, object) {
  var lights = currentRenderState.state.lights;
}

function setProgram(camera, fog, material, object) {
  var lights = currentRenderState.state.lights;
}


材质对象Material对应的着色器Shader代码

前面课程中讲解过每一种材质对象都对应着一个着色器代码,这节课来讲解Three.js渲染器解析材质对象Material,如何获得对应的着色器代码和uniforms对象。

  • 点材质PointsMaterial:顶点着色器文件points_vert.glsl、片元着色器文件points_frag.glsl
  • 基础网格材质MeshBasicMaterial:顶点着色器文件meshbasic_vert.glsl、片元着色器文件meshbasic_frag.glsl
  • 高光网格材质MeshPhongMaterial:顶点着色器文件meshphong_vert.glsl、片元着色器文件meshphong_frag.glsl

学习本节课的内容需要先简单阅读以下源码:

  • WebGLPrograms.js源码
  • \renderers\shaders目录下的着色器源码文件和js源码文件

材质对象封装

通过材质对象的.type属性,可以判断材质对象是哪种材质对象,一个材质对象具有一个惟一的type类型。

JavaScript语法

把字符串作为属性名访问对象的属性。

  var shaderIDs = {
  MeshBasicMaterial: 'basic',
  MeshLambertMaterial: 'lambert',
  MeshPhongMaterial: 'phong',
  };
  // 查看shaderIDs对象MeshBasicMaterial属性的值
  console.log(shaderIDs.MeshBasicMaterial);
  console.log('查看属性值',shaderIDs[ 'MeshBasicMaterial' ]);

shaders目录简介

着色器代码文件目录是three.js-master\src\renderers\shaders,shaders目录下有两个着色器代码的文件ShaderChunk和ShaderLib。

ShaderChunk目录下的着色器代码文件.glsl都是具有特定功能的模块,ShaderLib目录下的着色器文件会通过#include <ShaderChunk中着色器文件名>调用ShaderChunk目录下特定功能的着色器代码块构建出来具有具有特定功能的顶点着色器文件和片元着色器文件。

  • 点材质PointsMaterial:顶点着色器文件points_vert.glsl、片元着色器文件points_frag.glsl
  • 基础网格材质MeshBasicMaterial:顶点着色器文件meshbasic_vert.glsl、片元着色器文件meshbasic_frag.glsl
  • 高光网格材质MeshPhongMaterial:顶点着色器文件meshphong_vert.glsl、片元着色器文件meshphong_frag.glsl

ShaderChunk.js:用来获得ShaderChunk和ShaderLib文件中的着色器代码

ShaderLib.js:设置好点、线、网格材质对应的uniforms变量值、顶点着色器代码、片元着色器代码

UniformsLib.js、UniformsUtils.js:着色器中uniform变量对应的值

WebGLRenderer.js

通过WebGLPrograms对象的方法.getParameters()返回一个parameters对象,返回的parameters对象的shaderID属性保留了材质对象类型type的信息,通过材质对象信息可以在ShaderLib对象中获得材质对象对应的着色器代码。

import {ShaderLib} from './shaders/ShaderLib.js';
import {WebGLPrograms} from './webgl/WebGLPrograms.js';
programCache = new WebGLPrograms(_this, extensions, capabilities);

function initMaterial(material, fog, object) {

  // 返回一个parameters对象,具有shaderID属性,通过shaderID的属性值可以获得材质对象对应的着色器代码。
  var parameters = programCache.getParameters(material, lights.state, shadowsArray, ...object);

  // 通过shaderID键对应的值,作为ShaderLib对象的键名获得相应的值,uniforms对象、定点着色器代码、片元着色器代码
  var shader = ShaderLib[parameters.shaderID];

  materialProperties.shader = {
  name: material.type,
  uniforms: UniformsUtils.clone(shader.uniforms),
  vertexShader: shader.vertexShader,
  fragmentShader: shader.fragmentShader
  };
  // 处理着色器代码、编译着色器代码、返回程序对象program
  program = programCache.acquireProgram(material, materialProperties.shader, parameters, code);
}

WebGLPrograms.js

构造函数WebGLPrograms封装了.getParameters().getProgramCode().acquireProgram()等方法和.programs属性。

function WebGLPrograms( renderer, extensions, capabilities ) {
  var shaderIDs = {
  MeshDepthMaterial: 'depth',
  MeshDistanceMaterial: 'distanceRGBA',
  MeshNormalMaterial: 'normal',
  MeshBasicMaterial: 'basic',
  MeshLambertMaterial: 'lambert',
  MeshPhongMaterial: 'phong',
  MeshToonMaterial: 'phong',
  MeshStandardMaterial: 'physical',
  MeshPhysicalMaterial: 'physical',
  LineBasicMaterial: 'basic',
  LineDashedMaterial: 'dashed',
  PointsMaterial: 'points',
  ShadowMaterial: 'shadow'
};
this.getParameters = function (material, lights,...){
  // 通过材质对象.type值,从shaderIDs提取相应的属性值
  var shaderID = shaderIDs[ material.type ];

  var parameters = {
    // 该属性用于判断材质对象
  shaderID: shaderID,
  precision: precision,
  vertexColors: material.vertexColors,
  numSpotLights: lights.spot.length,
  numRectAreaLights: lights.rectArea.length,
  numHemiLights: lights.hemi.length,
  };

return parameters;
}
}

处理着色器shader代码

通过ShaderLib[parameters.shaderID]返回的着色器代码还需要一定的自动化处理才能正式作为和原生WebGL中一样的顶点、片元着色器代码使用。

学习本节课的内容需要先简单阅读以下源码:

  • WebGLShader.js源码
  • WebGLProgram.js源码
  • WebGLPrograms.js源码

WebGLShader.js——>WebGLProgram.js——>WebGLPrograms.js——>WebGLRenderer.js

WebGLRenderer.js

通过WebGLPrograms对象的方法.getParameters()返回一个parameters对象,返回的parameters对象的shaderID属性保留了材质对象类型type的信息,通过材质对象信息可以在ShaderLib对象中获得材质对象对应的着色器代码。

import {ShaderLib} from './shaders/ShaderLib.js';
import {WebGLPrograms} from './webgl/WebGLPrograms.js';
programCache = new WebGLPrograms(_this, extensions, capabilities);

function initMaterial(material, fog, object) {

  // 返回一个parameters对象,具有shaderID属性,通过shaderID的属性值可以获得材质对象对应的着色器代码。
  var parameters = programCache.getParameters(material, lights.state, shadowsArray, ...object);

  // 通过shaderID键对应的值,作为ShaderLib对象的键名获得相应的值,uniforms对象、定点着色器代码、片元着色器代码
  var shader = ShaderLib[parameters.shaderID];

  materialProperties.shader = {
  name: material.type,
  uniforms: UniformsUtils.clone(shader.uniforms),
  vertexShader: shader.vertexShader,
  fragmentShader: shader.fragmentShader
  };
  // 处理着色器代码、编译着色器代码、返回程序对象program
  program = programCache.acquireProgram(material, materialProperties.shader, parameters, code);
}

WebGLPrograms.js

构造函数WebGLPrograms封装了.getParameters().getProgramCode().acquireProgram()等方法和.programs属性。

  • .getParameters()方法:和ShaderLib.js封装的ShaderLib对象组合使用获得一个材质对象对应的着色器代码和uniforms对象。
  • .acquireProgram()方法:调用WebGLProgram构造函数,编译顶点和片元着色器代码,并创建返回对应的程序对象Program。
function WebGLPrograms( renderer, extensions, capabilities ) {
...
  this.acquireProgram = function ( material, shader, parameters, code ) {
    program = new WebGLProgram( renderer, extensions, code, material, shader, parameters );
    programs.push( program );
    return program;
  };
}
...

WebGLProgram.js

WebGLProgram.js封装了gl.attachShader()gl.deleteShader()gl.createProgram()gl.linkProgram()等原生WebGL API。

阅读WebGLProgram.js的源码需要对JavaScript语言的正则表达式和字符串的处理方法有一定的理解。

批量处理着色器代码:把着色器代码块中预先编写的表示光源数量的符号替换为具体的数字

// 替换灯的数量
function replaceLightNums( string, parameters ) {
// NUM_DIR_LIGHTS等字符出现在了.glsl文件着色器代码中,需要替换为具体的数字才能作为着色器代码使用。
	return string
		.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )
		.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )
		.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
		.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
		.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights );
}

把.glsl文件中#include <common>等字符串替换为相应.glsl文件中的代码字符串

function parseIncludes( string ) {
// “#include <着色模块名字>”对应的正则表达式
	var pattern = /^[ \t]*#include +<([\w\d.]+)>/gm;

	function replace( match, include ) {
		var replace = ShaderChunk[ include ];
		return parseIncludes( replace );
	}
// 参数string,也就是着色器代码字符串,执行replace方法
	return string.replace( pattern, replace );

}

顶点着色器前缀字符串prefixVertex,会和.glsl文件提供的顶点着色器代码拼接在一起。
通过WebGLPrograms.js封装的.getParameters()方法可以从材质对象提取相关的信息,返回一个包含材质信息的对象parameters,比如透明度、材质颜色等信息,着色器前缀会从parameters对象提取材质信息信息合成着色器代码。

// JavaScript数组对象的.join()方法会把数组中的字符串元素拼接成一个字符串返回
prefixVertex = [
  'precision ' + parameters.precision + ' float;',// precision highp float;
  'precision ' + parameters.precision + ' int;',// precision highp int;

  parameters.map ? '#define USE_MAP' : '',
  parameters.envMap ? '#define USE_ENVMAP' : '',
  // 材质vertexColors属性,默认值THREE.NoColors,查看\src\Three.js 可以知道是0
  // 如果材质属性设置了顶点渲染,属性值是THREE.VertexColors,着色器代码插入#define USE_COLOR   
  // 如果材质属性没设置顶点渲染:着色器代码中不会出现带预定义
  parameters.vertexColors ? '#define USE_COLOR' : '',

  parameters.flatShading ? '#define FLAT_SHADED' : '',//#define FLAT_SHADED   是否平面着色

  // 声明顶点着色器代码用到的uniform变量
  'uniform mat4 modelMatrix;',//模型矩阵
  'uniform mat4 modelViewMatrix;',
  'uniform mat4 projectionMatrix;',//投影矩阵
  'uniform mat4 viewMatrix;',//视图矩阵
  'uniform mat3 normalMatrix;',
  'uniform vec3 cameraPosition;',
  // 声明顶点着色器代码用到的顶点变量attribute
  'attribute vec3 position;',//顶点位置数据
  'attribute vec3 normal;',//顶点法向量数据
  'attribute vec2 uv;',//顶点UV坐标数据

  '\n'

].filter( filterEmptyLine ).join( '\n' );

片元着色器前缀字符串,会和.glsl文件提供的顶点着色器代码拼接在一起。

prefixFragment = [
// 片元精度定义
  'precision ' + parameters.precision + ' float;',
  'precision ' + parameters.precision + ' int;',
  parameters.map ? '#define USE_MAP' : '',
  parameters.envMap ? '#define USE_ENVMAP' : '',
// 视图矩阵   相机矩阵
  'uniform mat4 viewMatrix;',
  'uniform vec3 cameraPosition;',
].filter( filterEmptyLine ).join( '\n' );

顶点和片元着色器代码字符串的处理流程

function WebGLProgram( renderer,..., shader, parameters ) {
  // 预定义
  var defines = material.defines;
  // \shaders\ShaderLib目录下.glsl文件中顶点着色器、片元着色器代码字符串
	var vertexShader = shader.vertexShader;
  var fragmentShader = shader.fragmentShader;

  // 把顶点着色器中“#include <着色模块名字>”字符串替换为相应的着色器代码
  vertexShader = parseIncludes( vertexShader );
  // 把着色器代码中表示各种光源数量的字符串替换为表示该类光源对象数量的数字
  vertexShader = replaceLightNums( vertexShader, parameters );
  // 把着色器代码中表示剪裁平面数量相关的字符串替换为具体的数字
  vertexShader = replaceClippingPlaneNums( vertexShader, parameters );

  // 片元着色器代码和顶点着色器代码一样要进行类似的处理
  fragmentShader = parseIncludes( fragmentShader );
  fragmentShader = replaceLightNums( fragmentShader, parameters );
  fragmentShader = replaceClippingPlaneNums( fragmentShader, parameters );

  vertexShader = unrollLoops( vertexShader );
  fragmentShader = unrollLoops( fragmentShader );

  // .glsl文件中着色器代码字符串经过处理后与对应的着色器前缀字符串拼接
  var vertexGlsl = prefixVertex + vertexShader;
  var fragmentGlsl = prefixFragment + fragmentShader;

  // 处理完成的着色器代码进行编译处理,创建并返回对应的程序对象
  var glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
	var glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
  var program = gl.createProgram();
  gl.attachShader( program, glVertexShader );
  gl.attachShader( program, glFragmentShader );
  gl.linkProgram( program );
}

WebGLProgram对象的属性

this.name = shader.name;
this.id = programIdCount ++;
this.code = code;
this.usedTimes = 1;
this.program = program;
// WebGLShader函数返回值,顶点着色器对象
this.vertexShader = glVertexShader;
// WebGLShader函数返回值,片元着色器对象
this.fragmentShader = glFragmentShader;

WebGLShader.js

WebGLShader.js封装了gl.createShader()gl.shaderSource()gl.compileShader()等原生WebGL API。

执行WebGLShader函数可以返回一个gl.createShader()创建的着色器对象。

// WebGLShader.js源码
function WebGLShader( gl, type, string ) {
  // type是gl.VERTEX_SHADER:创建顶点着色器对象
  // type是gl.FRAGMENT_SHADER:创建片元着色器对象
	var shader = gl.createShader( type );
  // 引入顶点或片元着色器源代码string
	gl.shaderSource( shader, string );
  // 编译顶点或片元着色器
	gl.compileShader( shader );
  // 返回着色器对象
	return shader;
}        

WebGLProgram.js源码中会调用WebGLShader函数创建一个着色器对象。

function WebGLProgram( renderer,... material, shader, parameters ) {
  // 创建顶点着色器对象
  var glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
  // 创建一个片元着色器对象
  var glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
}

着色器字符串处理—材质属性、光源数量

材质的部分属性控制着着色器部分代码的生成,.glsl文件一些字符串表示某种光源对象的数量,需要替换为表示该光源的数量的数字。

WebGLLights.js

把光源对象按照种类进行分类,比如点光源一组、方向光源一组。

WebGLRenderer.js

通过WebGLPrograms对象的方法.getParameters()返回一个parameters对象,获得材质对象、渲染器对象、光源数量的信息。

import {WebGLPrograms} from './webgl/WebGLPrograms.js';
programCache = new WebGLPrograms(_this, extensions, capabilities);
function initMaterial(material, fog, object) {

  // 获得currentRenderState对象.lights属性的值:WebGLLights对象
  var lights = currentRenderState.state.lights;

  // 参数lights.state:WebGLLights对象的state属性包含了各类光源分类的集合,可以获得每一种光源的数量
  var parameters = programCache.getParameters(material, lights.state, shadowsArray, ...object);

}

WebGLPrograms.js

从材质对象、currentRenderState.state.lights提取信息设置为parameters对象属性的值。

// 参数lights:currentRenderState.state.lights.state
this.getParameters = function ( material, lights... ) {
// parameters:参数
// 着色器id号、precision精度、贴图map、vertexColors顶点渲染、numPointLights点光源数量...
// 从不同的对象提取数据   shaderID、材质对象、灯光对象、渲染器对象
  var parameters = {
    // JavaScript语法:!null=true  !200=false
    // map为null,!! material.map返回false,否则true
    map: !! material.map,
    envMap: !! material.envMap,
    // 获得材质vertexColors属性的值,默认是常量THREE.NoColors,也就是0(视频中错误纠正)
    // 查看\src\Three.js 文件可以知道THREE.NoColors、THREE.VertexColors表示的值
    vertexColors: material.vertexColors,
    ...
    // 获得光源对象数量
    numDirLights: lights.directional.length,
    numPointLights: lights.point.length,
    numSpotLights: lights.spot.length,
    numRectAreaLights: lights.rectArea.length,
    numHemiLights: lights.hemi.length,
    ...
  };
  return parameters;
};

WebGLProgram.js

.glsl文件着色器字符串插入代码

prefixVertex = [
...
  parameters.map ? '#define USE_MAP' : '',
  parameters.envMap ? '#define USE_ENVMAP' : '',
  // 材质vertexColors属性,默认值THREE.NoColors,查看\src\Three.js 可以知道是0
  // 如果材质属性设置了顶点渲染,属性值是THREE.VertexColors,着色器代码插入#define USE_COLOR   
  // 如果材质属性没设置顶点渲染:着色器代码中不会出现带预定义
  parameters.vertexColors ? '#define USE_COLOR' : '',
...
].filter( filterEmptyLine ).join( '\n' );

批量处理着色器代码:把着色器代码块中预先编写的表示光源数量的符号替换为具体的数字

// 替换灯的数量
function replaceLightNums( string, parameters ) {
// NUM_DIR_LIGHTS等字符出现在了.glsl文件着色器代码中,需要替换为具体的数字才能作为着色器代码使用。
	return string
		.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )
		.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )
		.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
		.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
		.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights );
}

数学相关

# Three.js向量
  为了让大家深入了解Three.js的Math模块API,本节课对向量内容展开讲解。
### Three.js向量相关API
- 二维向量[Vector2](http://www.yanhuangxueyuan.com/threejs/docs/index.html#api/math/Vector2)
- 三维向量[Vector3](http://www.yanhuangxueyuan.com/threejs/docs/index.html#api/math/Vector3)
- 四维向量[Vector4](http://www.yanhuangxueyuan.com/threejs/docs/index.html#api/math/Vector4)

几维向量就有几个分量,二维向量Vector2有x和y两个分量,也就是Vector2对象具有x和y两个属性,三维向量Vector3有xyz三个分量,四维向量Vector4有xyzw四个分量。在Threejs中一些对象属性值会使用这些向量对象来表示,比如几何体Geometry的顶点UV坐标需要表示一个二维坐标,所以uv坐标使用Vector2对象表示;几何体Geometry的顶点位置坐标在在三维空间笛卡尔坐标系中中坐标需要xyz三个分量,所以顶点坐标使用Vector3对象表示;Three.js模型对象的缩放属性可以在xyz三个方向上进行缩放,也就是说有三个分量需要表达,自然使用Vector3对象。

控制台查看Threejs对象位置、缩放属性的值
```JavaScript
var mesh = new THREE.Mesh()
mesh.position.set(100,20,330);//设置网格模型的位置
console.log('对象位置属性',mesh.position);
console.log('对象缩放属性',mesh.scale);

控制台查看几何体相关数据的表示方式

var geometry = new THREE.BoxGeometry(50,25,25);//立方体
console.log('几何体顶点位置坐标数据',geometry.vertices);
console.log('几何体三角形',geometry.faces);
console.log('几何体UV坐标',geometry.faceVertexUvs[0]);
创建向量对象

通过向量构造函数创建向量对象,查看向量对象的结构。

// 创建一个三维向量,xyz分量分别为3, 5, 4
var v1 = new THREE.Vector3(3, 5, 4)
console.log('向量x分量', v1.x);
// 重置向量的y分量
v1.y = 80;
// 重新设置三个分量
v1.set(2,4,8);

克隆.clone()和复制.copy()

对象执行克隆方法.clone(),返回一个新的对象,和原来对象属性的值一样。

var p1 = new THREE.Vector3(10,20,15);
var v1 = p1.clone();
console.log(`查看克隆的结果`,v1);

执行.copy()方法,向量p1三个分量xyz的值覆盖向量p2三个分量

var p1 = new THREE.Vector3(10,42,28);
var p2 = new THREE.Vector3();
p2.copy(p1);
console.log(`查看复制的结果`,p2);
向量和标量

如果你有一定的数学基础,肯定对向量和标量有一定的概念,比如说空间中一个点的位置是标量,两个点构成一个有方向的量是向量,在Three.js中,不一定要严格区分向量和标量的概念,THREE.Vector3对象既可以表示一个顶点位置,比如网格模型Mesh的位置坐标,也可以表示一个有方向的向量,比如顶点的法向量或光线的方向。虽然Vector字面意思是向量,但是THREE.Vector3对象表示的是向量还是标量,要看它的具体含义。

向量减法.sub()和向量长度.length()

通过.sub()方法可以对两个向量进行减法运算,比如两个表示顶点坐标的Vector3对象进行减法运算返回一个新的Vector3对象就是两个点构成的向量。

直接执行p1.sub(p2)会改变p1,所以先克隆然后再执行减法运算p1.clone().sub(p2)

向量对象执行.length()方法会返回向量的长度。

已知直线两个顶点的坐标,计算直线的长度.

// 点1坐标
var p1 = new THREE.Vector3(10,8,12);
// 点2坐标
var p2 = new THREE.Vector3(20,30,-10);
// .clone()方法克隆p1,直接执行向量减法.sub()会改变p1向量本身
// .sub():向量减法运算
// .length():返回向量的长度
var L = p1.clone().sub(p2).length();
console.log('两点之间距离',L);
点乘.dot()

向量的.dot()方法用来计算两个向量的点积,计算光线和几何体顶点夹角,几何体体积等等都会用到该方法。

已知三角形三个顶点的坐标,计算其中一个顶点对应角度余弦值。

// 三角形的三个点坐标p1,p2,p3
var p1 = new THREE.Vector3(0,0,0);// 点1坐标
var p2 = new THREE.Vector3(20,0,0);// 点2坐标
var p3 = new THREE.Vector3(0,40,0);// 点3坐标

// p1,p2两个点确定一个向量
var v1 = p1.clone().sub(p2);
// p1,p3两个点确定一个向量
var v2 = p1.clone().sub(p3);
// .dot()计算两个向量点积    .length()计算向量长度
// 返回三角形顶点p1对应夹角余弦值
var CosineValue = v1.dot( v2 ) /(v1.length()*v2.length())
console.log('三角形两条边夹角余弦值',CosineValue);
// .acos():反余弦函数,返回结果是弧度
console.log('三角形两条边夹角',Math.acos(CosineValue)*180/Math.PI);
叉乘.cross()

.crossVectors().cross()都是向量对象的叉乘计算方法,功能一样,只是使用的细节有些不同,向量对象叉乘的结果仍然是向量对象。

计算向量v1和v2的叉乘结果

// 声明一个向量对象,用来保存.crossVectors()方法结果
  var v3 = new THREE.Vector3();
  v3.crossVectors(v1,v2)

向量v2直接执行.cross()方法,叉乘的结果会覆盖向量v2的xyz分量

  v2.cross(v1)

克隆v2避免叉乘后改变原来的v2变量。

  var v3 = v2.clone();
  v3.cross(v1)

已知三角形的三个顶点p1, p2, p3的坐标值,利用三个顶点的坐标计算三角形的面积

//三角形面积计算
function AreaOfTriangle(p1, p2, p3){
  var v1 = new THREE.Vector3();
  var v2 = new THREE.Vector3();
  // 通过两个顶点坐标计算其中两条边构成的向量
  v1 = p1.clone().sub(p2);
  v2 = p1.clone().sub(p3);

  var v3 = new THREE.Vector3();
  // 三角形面积计算
  v3.crossVectors(v1,v2);
  var s = v3.length()/2;
  return s
}

Three.js矩阵

如果你有线性代数的基础,自然对象矩阵并不陌生,如果对大学所学的线性代数具体知识已经忘记了也没有关系,Three.js的矩阵库对常见的矩阵运算都进行了封装,如果不是为了封装或扩展3D引擎,只是开发一些常见3D项目,你只需要有一些基本的概念,会调用矩阵对象Matrix相关的方法就可以。

Three.js矩阵相关API

创建向量对象

通过矩阵对象的elements属性可以访问矩阵元素的具体指,如果创建的时候构造函数没有设置具体的值,构造函数实例化的时候会自动设置一个默认值。
4x4矩阵Matrix4

// 创建一个4x4矩阵对象
var mat4 = new THREE.Matrix4()
// 默认值单位矩阵
// 1, 0, 0, 0,
// 0, 1, 0, 0,
// 0, 0, 1, 0,
// 0, 0, 0, 1
console.log('查看矩阵对象默认值', mat4.elements);
属性elements和方法.set()

需要通过Matrix4对象表示的一个4x4矩阵

 | 1  0  0  5 |
 | 0  1  0  3 |
 | 0  0  1  9 |
 | 0  0  0  1 |

通过.set()方法重置矩阵的元素值,执行.set()方法,本质上改变的就是矩阵elements属性值,这里注意set方法的参数顺序是按行设置一个矩阵的元素值。

mat4.set(
  1, 0, 0, 5,
  0, 1, 0, 3,
  0, 0, 1, 9,
  0, 0, 0, 1
)

通过elements属性重置矩阵的元素值,elements的属性值是一个矩阵对象,里面的元素按列设置一个矩阵的元素值。

mat4.elements=[
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  5, 3, 9, 1
]
转置矩阵.transpose()

矩阵对象执行方法.transpose()按照一定算法改变的自身.elements属性值。

// 创建一个4X4矩阵
var mat4 = new THREE.Matrix4();
mat4.set(1,0,0,5,0,1,0,3,0,0,1,9,0,0,0,1);
//转置矩阵
mat4.transpose();
console.log('查看mat4的转置矩阵',mat4.elements);

为了不改变原矩阵,可以先执行克隆.clone方法返回一个和原矩阵完全一样的矩阵。

// 创建一个4X4矩阵
var mat4 = new THREE.Matrix4();
mat4.set(1,0,0,5,0,1,0,3,0,0,1,9,0,0,0,1);
// 先克隆,避免改变原矩阵mat4
var mat4T = mat4.clone()
//转置矩阵,
mat4T.transpose();
console.log('查看mat4的转置矩阵',mat4T.elements);
.multiplyScalar()

矩阵的每个元素乘以.multiplyScalar()方法的参数。

// 创建一个4X4矩阵
var mat4 = new THREE.Matrix4();
mat4.set(1,0,0,5,0,1,0,3,0,0,1,9,0,0,0,1);
// 矩阵乘以标量
mat4.multiplyScalar(10);
console.log('查看矩阵乘以标量后的结果', mat4.elements);
矩阵乘法
  • c.multiplyMatrices(a,b):参数中两个矩阵相乘axb,结果保存在c中
  • a.multiply(b):矩阵相乘axb,结果保存在a
  • a.premultiply(b):矩阵相乘bxa,结果保存在a
// 矩阵乘法运算
var mat41 = new THREE.Matrix4()
mat41.set(1,2,0,0,0,0,0,8,0,3,5,0,0,0,0,0)
var mat42 = new THREE.Matrix4()
mat42.set(1,2,0,0,0,2,3,0,7,0,0,8,0,3,5,0)
// mat43保存计算结果
var mat43 = new THREE.Matrix4()
// 矩阵乘法运算:mat41xmat42
mat43.multiplyMatrices(mat41,mat42)
console.log('查看mat43', mat43.elements);
逆矩阵getInverse

计算逆矩阵需要注意矩阵首先是可逆的,如果矩阵不可逆执行该方法会报错

计算可逆矩阵的逆矩阵

var mat4 = new THREE.Matrix4();
// 可逆矩阵
mat4.elements=[1,0,0,0,0,1,0,0,0,0,1,0,5,3,9,1]
// mat4I用来保存mat4逆矩阵计算结果
var mat4I = new THREE.Matrix4();
mat4I.getInverse(mat4, true);
// 控制台查看矩阵的逆矩阵
console.log('mat4I', mat4I.elements);

不可逆矩阵报错

var mat4 = new THREE.Matrix4();
// 不可逆矩阵
mat4.elements = [0,0,0,0,0,0,0,0,0,0,0,0,5,3,9,1];
// mat4I用来保存mat4逆矩阵计算结果
var mat4I = new THREE.Matrix4();
mat4I.getInverse(mat4, true);
// 控制台查看矩阵的逆矩阵
console.log('mat4I', mat4I.elements);

行列式.determinant()

通过.determinant()方法计算矩阵行列式的值

var mat = new THREE.Matrix4()
mat.set(1,2,0,0,0,2,3,0,7,0,0,8,0,3,5,0)
// 计算矩阵行列式的值
var determinant = mat.determinant()
console.log('行列式', determinant);
对象属性值

相关矩阵属性可以参考网格模型Mesh的基类Object3D。

var mesh = new THREE.Mesh();
console.log('本地矩阵', mesh.matrix);
console.log('世界矩阵', mesh.matrixWorld);
console.log('模型视图矩阵', mesh.modelViewMatrix);
// .normalMatrix属性值是3X3矩阵Matrix3
console.log('法线矩阵', mesh.normalMatrix);

相机对象的投影矩阵属性.projectionMatrix和视图矩阵属性.matrixWorldInverse

// 创建相机对象
var camera = new THREE.OrthographicCamera(-20, 20, 10, -10, 1, 1000);
console.log('视图矩阵', camera.matrixWorldInverse);
console.log('投影矩阵', camera.projectionMatrix);

Three.js旋转、平移、缩放矩阵

在WebGL中对一个对象进行平移、旋转或缩放本质就是对对象的顶点坐标进行平移、旋转、缩放矩阵变换。

关键词

在学习本节课之前最好对旋转、平移、缩放等变换矩阵有一定的了解,可以学习WebGL相关教程或图形学书籍。

平移矩阵

平移矩阵T:表示一个顶点坐标沿着X、Y、Z轴分别平移Tx、Ty、Tz

 | 1  0  0  Tx |
 | 0  1  0  Ty |
 | 0  0  1  Tz |
 | 0  0  0  1  |

一个点的坐标是(x,y,z),假设沿着X、Y、Z轴分别平移Tx、Ty、Tz,毫无疑问平移后的坐标是(x+Tx,y+Ty,z+Tz)。

矩阵和表示顶点坐标的向量进行乘法运算

 | 1  0  0  Tx |   | x |   | x+Tx |
 | 0  1  0  Ty | x | y | = | y+Ty |
 | 0  0  1  Tz |   | z |   | z+Tz |
 | 0  0  0  1  |   | 1 |   |  1   |
缩放矩阵

比如一个几何体的所有顶点坐标沿着X、Y、Z轴分别缩放矩阵Sx、Sy、Sz倍,可以用如下矩阵S表示。

 | Sx 0  0  0 |
 | 0  Sy 0  0 |
 | 0  0  Sz 0 |
 | 0  0  0  1 |

顶点坐标缩放变换

 | Sx 0  0  0 |   | x |   | x*Sx |
 | 0  Sy 0  0 | x | y | = | y*Sy |
 | 0  0  Sz 0 |   | z |   | z*Sz |
 | 0  0  0  1 |   | 1 |   |  1   |
旋转矩阵

绕x轴旋转α度对应的旋转矩阵Rx

 | 1  0     0     0 |   | x |   |       x       |
 | 0  cosα  -sinα 0 | x | y | = | cosα*y-sinα*z |
 | 0  sinα  cosα  0 |   | z |   | sinα*y+cosα*z |
 | 0  0     0     1 |   | 1 |   |        1      |

绕y轴旋转α度对应的旋转矩阵Ry

 | cosα  0  -sinα 0 |   | x |   |  cosα*x+sinα*z |
 | 0     1  0     0 | x | y | = |        y       |
 | sinα  0  cosα  0 |   | z |   | -sinα*x+cosα*z |
 | 0     0  0     1 |   | 1 |   |        1       |

绕z轴旋转α度对应的旋转矩阵Rz

 | cosα  -sinα 0  0 |   | x |   |  cosα*x-sinα*y |
 | sinα  cosα  0  0 | x | y |   |  sinα*x+cosα*y |
 | 0     0     1  0 |   | z |   |        z       |
 | 0     0     0  1 |   | 1 |   |        1       |

创建变换矩阵

直接通过矩阵对象的elements属性或.set()方法设置常见变换矩阵的元素比较麻烦,比如设置一个缩放矩阵,Three.js的4x4矩阵Matrix4对常见变换矩阵的设置封装了一些方法。

  • 绕x轴旋转.makeRotationX(theta)

  • 绕y轴旋转.makeRotationY(theta)

  • 绕z轴旋转.makeRotationZ(theta)

  • 缩放.makeScale(Sx,Sy,Sz)

  • 平移.makeTranslation(Tx,Ty,Tz)

  • 剪切.makeShear

平移矩阵创建案例
 | 1  0  0  5 |
 | 0  1  0  3 |
 | 0  0  1  9 |
 | 0  0  0  1 |

.set()方法设置平移矩阵

var T = new THREE.Matrix4()
// set方法设置平移矩阵
T.set(
  1, 0, 0, 5,
  0, 1, 0, 3,
  0, 0, 1, 9,
  0, 0, 0, 1
)

makeTranslation方法设置平移矩阵

var T = new THREE.Matrix4()
// 顶点坐标沿着X、Y、Z轴分别平移5,3,9
T.makeTranslation(5,3,9)
console.log('查看平移矩阵', T.elements);

向量矩阵变换.applyMatrix4()

.applyMatrix4()是三维向量Vector3的一个方法,

var T = new THREE.Matrix4()
// 创建一个平移矩阵,顶点坐标沿着X、Y、Z轴分别平移5,3,9
T.makeTranslation(5, 3, 9)
// 三维向量表示一个顶点坐标
var v1 = new THREE.Vector3(10,10,10);
// 向量进行矩阵变换
var v2 = v1.clone().applyMatrix4(T);
console.log('查看平移后坐标', v2);

多次变换

顶点坐标经过多次变换可以把多个变换矩阵进行乘法运算,然后再和表示顶点的坐标进行变换。

模型矩阵M、平移矩阵T、缩放矩阵S、旋转矩阵R、绕X轴旋转矩阵Rx、绕X轴旋转矩阵Ry、绕X轴旋转矩阵Rz

旋转矩阵: R = RxRyRz

模型矩阵:M = RST

顶点V1执行模型矩阵变换:V2 = M*V1

顶点进行两次平移变换代码

// 创建平移矩阵T1:x轴平移100
var T1 = new THREE.Matrix4().makeTranslation(100, 0, 0)
// 创建平移矩阵T2:y轴平移100
var T2 = new THREE.Matrix4().makeTranslation(0, 100, 0)

// 两个变换矩阵相乘表示顶点先后经过两次
var M = new THREE.Matrix4()
M.multiplyMatrices(T2,T1)
// 三维向量表示一个顶点坐标
var v1 = new THREE.Vector3(10, 10, 10);
// 向量进行矩阵变换
var v2 = v1.clone().applyMatrix4(M);
console.log('查看平移后坐标', v2);
Object3D本地矩阵属性.matrix

通过前面知识的学习,应该都知道对象Object3D的位置.position、缩放.scale、角度.rotation等属性,这些属性本质上都是矩阵变换。Object3D对象的.translateX().translateZ()等平移方法会改变.position属性的值,Object3D对象的.rotateX().rotateZ()等旋转方法会改变.rotation属性的值,Three.js渲染模型解析的时候,Three.js会解析Object3D对象位置.position、缩放.scale、角度.rotation属性对应的平移、旋转、缩放矩阵相乘转化为本地矩阵.matrix的属性值。

执行旋转方法.rotateZ()查看,查看角度属性.rotation属性值欧拉对象z属性的变化

// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 绕z轴旋转
mesh.rotateZ(Math.PI)
console.log('查看角度属性值的变化',mesh.rotation);

执行平移方法.translateX()查看,查看位置.position属性值x分量变化

// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 沿着x轴平移100
mesh.translateX(100)
console.log('查看位置属性的变化',mesh.position);

查看位置.position、缩放.scale、角度.rotation等属性对本地矩阵属性.matrix的影响。
Three.js渲染的时候会把模型的矩阵值传递给着色器对顶点进行矩阵变换,具体细节这里不展开讲解。

// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 缩放网格模型
mesh.scale.set(900,900,900)
// 位置、角度、缩放属性值更新到矩阵属性matrix
mesh.updateMatrix()
console.log('查看本地矩阵属性matrix',mesh.matrix.elements);

投影矩阵、视图矩阵

学习本节课之前最好对图形学中视图矩阵和投影矩阵有一定了解,同时对于Three.js的正投影相机OrthographicCamera、透视投影相机PerspectiveCamera有一定了解。

关键词:投影矩阵、视图矩阵、正投影、透视投影、视点、目标观察点、上方向、正投影相机、透视投影相机

如果你对图形学中视图矩阵、投影矩阵相关内容比较了解有助于本节课的学习,如果不了解的话可以根据关键词去检索一下相关的内容去学习补充。

相机对象属性.matrixWorldInverse.projectionMatrix

正投影相机PerspectiveCamera和透视投影相机OrthographicCamera的基类是相机Camera,相机对象Camera具有视图矩阵属性.matrixWorldInverse和投影矩阵属性.projectionMatrix

相机对象本质就是视图矩阵和投影矩阵,顶点坐标经过平移旋转缩放模型变换以后,还需要经过视图、投影变换才能显示到画布上。

Matrix4方法:正投影.makeOrthographic()

正投影公式:

\begin{bmatrix}
\frac{2}{right-left} & 0& 0& -\frac{right+left}{right-left}& \
0& \frac{2}{top-bottom}& 0& -\frac{top+bottom}{top-bottom}& \
0& 0& -\frac{2}{far-near}& -\frac{far+near}{far-near}& \
0& 0& 0& 1&
\end{bmatrix}

矩阵对象Matrix4的方法.makeOrthographic()封装了正投影的算法,该方法用来创建一个正投影矩阵,在正投影相机对象OrthographicCamera中会调用该方法更新相机对象的投影矩阵属性.projectionMatrix

方法参数:.makePerspective( left,right,top,bottom,near,far)

正投影相机OrthographicCamera

正投影相机OrthographicCamera类封装调用了矩阵对象Matrix4的正投影矩阵变换方法.makeOrthographic()。执行该方法用来改变正投影相机对象的投影矩阵属性.projectionMatrix

// OrthographicCamera.js源码
this.projectionMatrix.makeOrthographic( left, right, top, bottom, this.near, this.far )

构造函数PerspectiveCamera(left,right,top,bottom,near,far)

正投影相机设置例子

var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 150; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
Matrix4方法:透视投影矩阵.makePerspective()

透视投影公式:

\begin{bmatrix}
\frac{near}{right} & 0& 0& 0& \
0& \frac{near}{top}& 0& 0& \
0& 0& -\frac{far+near}{far-near}& -\frac{2farnear}{far-near}& \
0& 0& -1& 0&
\end{bmatrix}

矩阵对象Matrix4的方法.makePerspective()封装了透视投影的算法,该方法用来创建一个透视投影矩阵,在透视投影相机对象PerspectiveCamera中会调用该方法更新相机对象的投影矩阵属性.projectionMatrix

方法参数:.makePerspective( left,right,top,bottom,near,far)

透视投影相机PerspectiveCamera

透视投影相机PerspectiveCamera类封装调用了矩阵对象Matrix4的透视投影矩阵变换方法.makePerspective()。执行该方法用来改变透视投影相机对象的投影矩阵属性.projectionMatrix

// PerspectiveCamera.js源码
this.projectionMatrix.makePerspective(...);

构造函数PerspectiveCamera(fov,aspect,near,far)

透视投影相机使用例子

var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
/**透视投影相机对象*/
var camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
Matrix4方法:.lookAt()

矩阵对象Matrix4.lookAt()方法对图形学中投影矩阵算法进行了封装,也就是通过给定的参数生成变换矩阵,视图矩阵和模型矩阵一样会用于场景中对象的平移旋转等变换,该方法通常用于构建相机对象的视图矩阵.matrixWorldInverse属性。

参数:.lookAt(eye,center, up )

三个参数都是三维向量对象Vector3,eye是视点也就是观察位置,center表示被观察的位置,up表示向上的方向。

Object3D方法.lookAt(x,y,z)

Object3D类封装了矩阵对象Matrix4.lookAt()方法,得到一个新的方法.lookAt(x,y,z),参数表示xyz是相机的目标观察点。

通过Object3D对象的.lookAt(x,y,z)方法可以改变自身的四元数属性.quaternion,四元数属性.quaternion和对象角度属性rotaion一样表示对象的旋转变换,可以转化为旋转矩阵,进而改变对象的本地矩阵属性.matrix和世界矩阵属性.matrixWorld

// Object3D.js源码
// `.lookAt()`方法计算得到的旋转矩阵对象m1改变对象的四元数属性.quaternion
this.quaternion.setFromRotationMatrix( m1 );
相机对象

透视投影相机PerspectiveCamera和正投影相机OrthographicCamera的基类是相机对象Camera,相机对象的基类是Object3D,所以相机对象会继承Object3D.lookAt(x,y,z)方法,勇于改变自身的矩阵属性。

Object3DCameraPerspectiveCamera

Object3DCameraOrthographicCamera

相机对象的视图矩阵属性matrixWorldInverse,字面意思是世界矩阵逆矩阵的意思,这可以看书来相机对象的视图矩阵属性就是自身世界矩阵matrixWorld的逆矩阵。

设置相机对象的位置属性和lookAt方法本质就是改变自身的视图矩阵属性matrixWorldInverse

var camera = new THREE.OrthographicCamera(...);
//设置相机位置
camera.position.set(200, 300, 200);
//设置相机对象的观察目标的位置
camera.lookAt(scene.position);

Three.js包围盒

包围盒是一种计算一系列顶点最优包围空间的算法,比如一个不规则几何体的所有顶点坐标都被包围在一个最小的长方体盒子中,需要一个算法来求解这个最小的长方体包围盒。Three.js封装与包围盒算法相关的三个API,分别是三维包围盒Box3、包围球Sphere、包围矩形Box2。

包围矩形Box2

包围矩形对象Box2表示一个矩形区域,使用min和max两个属性来描述该矩形区域,属性值都是二维向量对象Vector2,通过Box2的构造函数可以直接设置min和max属性值,也可以通过Box2的一些方法来设置。

描述一个矩形区域需要通过xy坐标来表示,X范围[Xmin,Xmax],Y范围[Ymin,Ymax],.min属性值是Vector2(Xmin, Ymin),.max属性值是Vector2(Xmax, Ymax).

通过参数设置min和max属性

// 参数1对应min属性值,参数2对应max属性值
var box2 = new THREE.Box2(new THREE.Vector2(0, 0), new THREE.Vector2(25, 25))
console.log('box2',box2);
box2.min=new THREE.Vector2(0, 0);
box2.max=new THREE.Vector2(25, 25);

设置min和max属性值

box2.min=new THREE.Vector2(0, 0);
box2.max=new THREE.Vector2(25, 25);
包围盒Box3

包围盒Box3表示三维长方体包围区域,使用min和max两个属性来描述该包围区域,Box3的min和max属性值都是三维向量对象Vector3。

描述一个长方体包围盒需要通过xyz坐标来表示,X范围[Xmin,Xmax],Y范围[Ymin,Ymax],Z范围[Zmin,Zmax],.min属性值是Vector3(Xmin, Ymin, Zmin),.max属性值是Vector3(Xmax, Ymax, Zmin).

var box3 = new THREE.Box3()
console.log('box3',box3);
box3.min = new THREE.Vector3(-10, -10,0);
box3.max = new THREE.Vector3(100, 20,50);
包围球Sphere

包围球Sphere是一个球形的包围区域,通过球心坐标.center和半径.radius两个属性来描述.

// 创建一个包围球对象,球心默认坐标原点,半径默认0.
var sphere = new THREE.Sphere()
console.log('sphere', sphere);
// 设置球心坐标
sphere.center=new THREE.Vector3(-10, -10,0);
// 设置包围球半径
sphere.radius=20;
Box3方法.setFromPoints()

包围盒Box3方法.setFromPoints()用来计算一系列顶点集合的最小包围盒,参数是表示顶点坐标的三维向量Vector3作为元素构成的数组对象。

// 通过球体API创建一个几何体,本质上就是一系列沿着球面分布的顶点
var geometry = new THREE.SphereGeometry(50, 100, 100);
// 创建一个包围盒对象Box3
var box3 = new THREE.Box3()
// 计算点集geometry.vertices的包围盒
box3.setFromPoints(geometry.vertices);
console.log('box3', box3);
几何体方法.computeBoundingBox()

几何体Geometry调用Box3的方法.setFromPoints()封装了一个方法.computeBoundingBox(),用来计算几何体的包围盒属性.boundingBox

几何体包围盒属性.boundingBox默认值为空null,执行.computeBoundingBox()方法才会计算该几何体的包围盒Box3,然后赋值给.boundingBox属性。

几何体包围球属性.boundingSphere使用方式和包围盒属性.boundingBox一样。

var geometry = new THREE.SphereGeometry(50, 100, 100); //球体
// .computeBoundingBox()方法计算.boundingBox的属性值
geometry.computeBoundingBox();
console.log('包围盒属性', geometry.boundingBox);
// 包围球相关属性和计算方法和包围盒一样
geometry.computeBoundingSphere();
console.log('包围球属性', geometry.boundingSphere);
几何体居中方法center()

在空间坐标系中把几何体居中,也就是几何体对应的包围盒中心平移到坐标原点。

// 几何体的中心默认与坐标原点重合
var geometry = new THREE.BoxGeometry(50, 50, 50);
// 几何体沿着x轴平移50,几何体的顶点坐标变化
geometry.translate(50, 0, 0);
// 居中:偏移的几何体居中
geometry.center();
Box3方法.expandByObject()

获得层级模型的包围盒,一个层级模型可能包含多个子孙后代,具体点说,比如一个Group对象有多个网格模型Mesh作为子对象。

加载一个层级模型,并计算它的包围盒

var loader = new THREE.ObjectLoader();
loader.load('group.json', function(group) {
  scene.add(group);//加载返回的模型对象插入场景
  var box3 = new THREE.Box3()
  // 计算层级模型group包围盒
  box3.expandByObject(group)
  console.log('查看包围盒box3', box3);
})
Box3方法.expandByScalar()

包围盒整体尺寸放大

// 缩放包围盒,尺寸放大1.5倍
box3.expandByScalar(1.5)
Box3方法.getSize()

返回包围盒具体的长宽高尺寸

var v3 = new THREE.Vector3()
// 获得包围盒长宽高尺寸,结果保存在参数三维向量对象v3中
box3.getSize(v3)
console.log('查看返回的包围盒尺寸', v3);
Box3方法.getCenter()

计算返回包围盒几何中心

// 计算一个层级模型对应包围盒的几何体中心
var center = new THREE.Vector3()
box3.getCenter(center)
console.log('查看几何体中心坐标', center);
Sphere方法.getBoundingSphere()

包围盒Box3和包围球Sphere可以相互等价转化,通过包围盒对象来计算包围球对象

var sphere = new THREE.Sphere()
// 计算包围盒box3对应的包围球
box3.getBoundingSphere(sphere)
console.log('查看通过box3重置的sphere', sphere);

欧拉对象Euler和四元数Quaternion

欧拉对象和四元数主要用来表达对象的旋转信息。

关键词:欧拉Euler、四元数Quaternion、矩阵Matrix4

欧拉对象Euler

构造函数:Euler(x,y,z,order)
参数xyz分别表示绕xyz轴旋转的角度值,角度单位是弧度。参数order表示旋转顺序,默认值XYZ,也可以设置为YXZYZX等值

// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
var Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);

设置欧拉对象的

var Euler = new THREE.Euler();
Euler.x = Math.PI/4;
Euler.y = Math.PI/2;
Euler.z = Math.PI/4;

四元数Quaternion

四元数对象Quaternion使用x、y、z和w四个分量表示。

var quaternion = new THREE.Quaternion();
console.log('查看四元数结构',quaternion);
console.log('查看四元数w分量',quaternion.w);

四元数方法.setFromAxisAngle()

四元数的方法.setFromAxisAngle(axis, angle)通过旋转轴axis和旋转角度angle设置四元数数据,也就是x、y、z和w四个分量。

绕单位向量Vector3(x,y,z)表示的轴旋转θ度

k = sinθ/2

q=( xk , yk , z*k, cosθ/2)

var quaternion = new THREE.Quaternion();
// 旋转轴new THREE.Vector3(0,1,0)
// 旋转角度Math.PI/2
quaternion.setFromAxisAngle(new THREE.Vector3(0,1,0),Math.PI/2)
console.log('查看四元数结构',quaternion);

四元数乘法.multiply()

对象的一个旋转可以用一个四元数表示,两次连续旋转可以理解为两次旋转对应的四元数对象进行乘法运算。

// 四元数q1、q2分别表示一个旋转,两个四元数进行乘法运算,相乘结果保存在q2中
// 在q1表示的旋转基础在进行q2表示的旋转操作
q1.quaternion.multiply( q2 );

欧拉、四元数和矩阵转化

欧拉对象、四元数对象和旋转矩阵可以相关转化,都可以表示旋转变换。

Matrix4.makeRotationFromQuaternion(q)

通过矩阵对象Matrix4.makeRotationFromQuaternion(q)方法可以把四元数转化对应的矩阵对象。

quaternion.setFromEuler(Euler)

通过欧拉对象设置四元数对象

Euler.setFromQuaternion(quaternion)

四元数转化为欧拉对象

Object3D

Object3D对象角度属性.rotation的值是欧拉对象Euler,四元数属性.quaternion的值是四元数对象Quaternion

执行Object3D对象旋转方法,会同时改变对象的角度属性和四元数属性。四元数属性和位置.position、缩放属性.scale一样会转化为对象的本地矩阵属性.matrix,本地矩阵属性值包含了旋转矩阵、缩放矩阵、平移矩阵。

Object3D对象角度属性.rotation和四元数属性.quaternion是相互关联的一个改变会同时改变另一个。

// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 绕z轴旋转
mesh.rotateZ(Math.PI)

console.log('查看角度属性rotation',mesh.rotation);
console.log('查看四元数属性quaternion',mesh.quaternion);

.rotateOnAxis(axis, angle)表示绕绕着任意方向某个轴axis旋转一定角度angle,绕X、Y和Z轴旋转对应的方法分别是rotateX()rotateY()rotateZ(),绕着XYZ特定轴旋转的方法是基于.rotateOnAxis()方法实现的。

// Object3D.js源码
rotateOnAxis: function () {
  var q1 = new Quaternion();
// 旋转轴axis,旋转角度angle
  return function rotateOnAxis( axis, angle ) {
// 通过旋转轴和旋转角度设置四元数的xyzw分量
    q1.setFromAxisAngle( axis, angle );
// Object3D对象的四元数属性和四元数q1相乘
    this.quaternion.multiply( q1 );

    return this;

  };

}(),

几何计算相关API

Threejs封装了一些和几何计算相关的API,比如线段Line3、三角形Triangle、射线Ray、平面Plane…

线段Line3

通过起始点定义一条线段。

// 创建一个线段对象Line3
var line3 = new THREE.Line3();
// 线段起点坐标
line3.start = new THREE.Vector3(0, 0, 0);
// 线段终点坐标
line3.end = new THREE.Vector3(10, 10, 10);

计算线段中点,或者说计算两点的中点

// 创建一个三维向量对象表示线段中点
var center = new THREE.Vector3();
// 执行getCenter方法计算线段中点,结果保存到参数
line3.getCenter(center)
console.log('查看线段中点', center);

计算线段的距离,或者说计算两点之间的距离

// 计算线段长度
var L = line3.distance();
console.log('查看线段距离', L);
// 计算线段长度平方
var L2 = line3.distanceSq();
console.log('查看线段距离平方', L2);

可以通过向量对象Vector3.distanceTo()方法计算两点之间距离

// 线段起点坐标
var p1 = new THREE.Vector3(0, 0, 0);
// 线段终点坐标
var p2 = new THREE.Vector3(10, 10, 10);
// Vector3的方法distanceTo()计算两点之间距离
var length = p1.distanceTo(p2)
console.log('两点之间距离', length);

射线Ray

// 创建射线对象Ray
var ray = new THREE.Ray()
// 设置射线起点
ray.origin = new THREE.Vector3(1,0,3);
// 设置射线方向
ray.direction = new THREE.Vector3(1,1,1).normalize();

通过射线Ray.intersectTriangle()方法判断射线和一个三角形区域是否相交,如果相交返回交点坐标,不相交返回null。

// 三角形三个点坐标
var p1 = new THREE.Vector3(20, 0, 0);
var p2 = new THREE.Vector3(0, 0, 10);
var p3 = new THREE.Vector3(0, 30, 0);
// 相交返回交点,不相交返回null
var result = ray.intersectTriangle(p1,p2,p3)
console.log('查看是否相交', result);

通过射线Ray.intersectsBox(Box3)方法判断射线和一个包围盒Box3是否相交,通过射线Ray.intersectsSphere(Sphere)方法判断射线和一个包围球Sphere是否相交…

三角形Triangle

// 创建一个三角形对象
var Triangle = new THREE.Triangle()
// 三角形顶点1
Triangle.a = new THREE.Vector3(20, 0, 0);
// 三角形顶点2
Triangle.b = new THREE.Vector3(0, 0, 10);
// 三角形顶点3
Triangle.c = new THREE.Vector3(0, 30, 0);

通过三角形对象Triangle.getArea()方法可以计算一个三角形区域的面积,如果你想计算一个网格模型的表面,就可以遍历网格模型对应几何体所有的三角形区域计算面积然后累加。

// .getArea()方法返回三角形面积
var S = Triangle.getArea();
console.log('三角形面积', S);

通过三角形对象Triangle.getMidpoint()方法计算三角形重心,封装的算法就是三个顶点坐标的算术平均值。

var Midpoint = new THREE.Vector3();
// 计算三角形重心,结果保存在参数Midpoint
Triangle.getMidpoint(Midpoint);
console.log('三角形重心', Midpoint);

通过三角形对象Triangle.getNormal()方法计算三角形法线方向,封装的算法简单说就是两条边构成的向量叉乘后获得垂直三角形面的向量。

var normal = new THREE.Vector3();
// 计算三角形法线方向,结果保存在参数normal
Triangle.getNormal(normal);
console.log('三角形法线', normal);

平面Plane

通过平面法线方向.normal和平面到坐标原点距离.constant来定义一个平面对象Plane

// 创建一个平面对象Plane
var plane = new THREE.Plane();
// 设置平面法线方向
plane.normal = new THREE.Vector3(0, 1, 0);
// 坐标原点到平面的距离,区分正负
plane.constant = 30;

执行平面对象方法.setFromCoplanarPoints(a,b,c)通过三个顶点坐标来设置一个平面对象Plane,三个点按照逆时针顺序来确定平面对象的法向量normal方向。

// 创建一个平面对象Plane
var plane = new THREE.Plane();
// 三个点坐标
var p1 = new THREE.Vector3(20, 0, 0);
var p2 = new THREE.Vector3(0, 0, 10);
var p3 = new THREE.Vector3(0, 30, 0);
// 通过三个点定义一个平面
plane.setFromCoplanarPoints(p1,p2,p3);
console.log('plane.normal', plane.normal);
console.log('plane.constant', plane.constant);

通过平面对象的.distanceToPoint(point)方法计算点到平面的垂线距离。

var point = new THREE.Vector3(20, 100, 330);
// 计算空间中一点到平面的垂直距离
var L = plane.distanceToPoint(point);
console.log('点到平面距离', L);

着色器编程


Three.js自定义着色器Shader

学习Three.js的着色器的内容之前,最好有一些WebGL的基础,可以不深入了解,但是要对WebGL渲染流程和着色器语言GLSL有一定的基本认知。如果你没有WebGL基础,可以学习下本站的WebGL视频教程。

MeshPhongMaterial、PointsMaterial等three.js的材质材质对象本质上都是着色器代码,
Three.js的WebGL渲染器在调用渲染方法render渲染场景的时候,会根据材质的type值调用路径
src\renderers\shaders下的着色器代码编译后在GPU中执行。

Three.js提供了RawShaderMaterialShaderMaterial两个API用来辅助开发者自定义着色器代码。这个着色器API和其它的three.js的材质对象的基类一样都是Material,会继承基类Material的属性和方法。

视频和源码

视频讲解和源码下载——Three.js视频教程进阶部分

ShaderMaterial

ShaderMaterial构造函数的参数和其它材质对象构造函数一样是一个对象,参数对象包含一些特定的属性,执行构造函数参数对象的属性会转化为材质对象对应的属性。

ShaderMaterial顶点着色器和片元着色器属性

GPU的顶点着色器单元用来处理顶点位置、顶点颜色、顶点向量等等顶点数据,片元着色器单元用来处理片元(像素)数据。一个WebGL程序的着色器代码包含顶点着色器和片元着色器,顶点着色器代码运行在GPU的顶点着色器单元,片元着色器代码运行在片元着色器单元。

ShaderMaterial对象具有两个用来设置自定义着色器代码的属性,顶点着色器属性vertexShader和片元着色器属性fragmentShader,顶点着色器属性vertexShader的属性值是顶点着色器代码字符串,片元着色器属性fragmentShader的属性值是片元着色器代码字符串。

着色器代码编写

通过three.js的着色器材质构造函数ShaderMaterial编写着色器代码和原生WebGL中编写着色器代码语法上是一样的,不同的地方在于更加方便,有些代码不用自己写,Three.js渲染器会帮你自动设置一些代码,比如声明一些常见的变量,通常来说在顶点着色器中把表示顶点的位置数据的变量position赋值给着色器内置变量gl_Position,需要首先声明attribute vec3 position;,如果使用ShaderMaterial构造函数,则不用程序员手动声明position变量,Three.js渲染器后自动帮你拼接一段该代码,具体的原理可以参考路径three.js-master\src\renderers\webgl\WebGLProgram.js下的WebGLProgram.js代码模块,Threejs渲染器在渲染场景的时候从ShaderMaterial提取着色器代码后,会拼接一段前缀字符串,然后才会传入GPU中执行,前缀包含一些常用的attribute变量和uniform变量。关于着色器材质对象ShaderMaterial的一些系统自动化处理的地方这里先不展开讲解,后面会逐步讲解。

顶点着色器代码
<script id="vertexShader" type="x-shader/x-vertex">
  // 使用ShaderMaterial类,顶点位置变量position无需声明,顶点着色器可以直接调用
  // attribute vec3 position;
  void main(){
    // 逐顶点处理:顶点位置数据赋值给内置变量gl_Position
    gl_Position = vec4( position, 1.0 );
  }
</script>
片元着色器代码
<script id="fragmentShader" type="x-shader/x-fragment">
  void main() {
    // 逐片元处理:每个片元或者说像素设置为红色
    gl_FragColor = vec4(1.0,0.0,0.0,1.0);
  }
</script>

设置vertexShaderfragmentShader属性值

通过元素.textContent属性返回<script>标签中着色器代码字符串,然后把着色器字符串赋值给ShaderMaterial材质对象对应的属性。

var material = new THREE.ShaderMaterial({
  vertexShader: document.getElementById('vertexShader').textContent,
  fragmentShader: document.getElementById('fragmentShader').textContent,
});

ShaderMaterial和其它Three.js的材质一样作为网格模型或点线模型对象的参数使用。

var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中

顶点数据自动化传递

在原生WebGL代码中,如果顶点或片元着色器代码如果声明了一个变量,比如顶点着色器中声明了一个顶点位置变量attribute vec3 position;,需要通过WebGL API把JavaScript中的几何体顶点位置数据传递给顶点着色器中的顶点位置变量position,这样的话,CPU执行顶点着色器代码的时候才能够处理顶点数据。

使用ShaderMaterial API的好处就是这个过程Three.js渲染器系统会自动解析几何体对象Geometry中顶点位置、颜色、法向量等数据,然后传递给着色器中的相应变量。具体的解析过程可以参考路径three.js-master\src\renderers\下的渲染器代码WebGLRenderer.js文件和路径three.js-master\src\renderers\webgl下面多个three.js文件。

WebGL渲染器在解析模型几何体中顶点数据的时候,Geometry类型的几何体会自动转化为缓冲类型的几何体BufferGeometry,
BufferGeometry几何体对象具有.attributes属性,BufferGeometry.attributes具有顶点位置、顶点法向量、顶点uv坐标等属性,对应着色器中相应的attribute变量。

可以通过BufferGeometryGeometryAPI创建一个空的几何体,然后手动设置顶点数据,也可以使用一些立方体或其他几何体的API创建一个几何体API,使用这些几何体API的时候,会自动生成顶点的相关数据,然后渲染的时候,WebGL渲染器自动传递给着色器中声明的相应变量。关于几何体BufferGeometry和顶点相关知识这里不再展示详述,有不理解的地方可以多学习原生WebGL教程和本站Threejs视频教程中关于几何体顶点讲解的章节。

var geometry = new THREE.BufferGeometry(); //创建一个Buffer类型几何体对象
var vertices = new Float32Array([
  0.6, 0.2, 0, //顶点1坐标
  0.7, 0.6, 0, //顶点2坐标
  0.8, 0.2, 0, //顶点3坐标
  -0.6, -0.2, 0, //顶点4坐标
  -0.7, -0.6, 0, //顶点5坐标
  -0.8, -0.2, 0, //顶点6坐标
]);
// 创建属性缓冲区对象  3个为一组,表示一个顶点的xyz坐标
var attribue = new THREE.BufferAttribute(vertices, 3);
// 设置几何体attributes属性的位置属性
geometry.addAttribute( 'position', attribue );

RawShaderMaterial

原生着色器材质对象RawShaderMaterial和着色器材质对象ShaderMaterial一样具有顶点着色器和片元着色器属性,同样可以自动传递顶点数据,区别在于着色器中使用的一些常见attribute或uniform变量,原生着色器材质对象RawShaderMaterial需要程序员手动编写,系统不会自动化添加变量声明的前缀。

顶点着色器代码,自动声明顶点位置属性position变量。

<script id="vertexShader" type="x-shader/x-vertex">
  attribute vec3 position;
  void main(){
    gl_Position = vec4( position, 1.0 );
  }
</script>

绘制模式

如果你对WebGL或OpenGL有一点了解,应该都知道,一系列的顶点数据可以通过绘制模式来控制渲染效果,一个顶点数据可以渲染为一个点,也可以使用线条模式把点连成线绘制出来,也可以通过三角形模式每三个点绘制一个三角面来,一系列三角形构成一个网格模型。
Three.js渲染器解析渲染的时候会根据模型的类型来判断如何渲染,解析点模型Points的时候,会启用点渲染模式,解析线模型LineLineLoopLineSegments的时候,会启用对应的线条绘制模式,解析网格模型Mesh会启用三角形绘制模式。

点绘制模式,在顶点着色器代码中可以通过设置内置变量gl_PointSize设置点的渲染大小,如果直线或三角形绘制模式不需要内置变量gl_PointSize

var point = new THREE.Points(geometry, material);
void main(){
  gl_PointSize=20.0;// 控制渲染的点大小
  gl_Position = vec4( position, 1.0 );
}

直线绘制模式,连点成线

var line = new THREE.Line(geometry, material);

三角形绘制模式,三个顶点确定一个三角形,一个个三角形区域构成一个网格模型

var mesh = new THREE.Mesh(geometry, material);

Three.js着色器——矩阵变换

本节课讲解如何通过ShaderMaterial编写顶点矩阵变换的代码,Three.js的渲染器解析场景和相机参数进行渲染的时候,会从模型对象获得几何体顶点对应的模型矩阵modelMatrix,从相机对象获得视图矩阵viewMatrix和投影矩阵projectionMatrix,在着色器中通过获得模型矩阵、视图矩阵、投影矩阵对顶点位置坐标进行矩阵变换。

本节课不会详细去解释模型矩阵、视图矩阵、投影矩阵的具体算法,主要是讲解这些矩阵在three.js自定义着色器的时候,如何更好的使用。

模型变换

模型矩阵包含了一个几何体的旋转、平移、缩放变换。

如果你有基本的图形学和线性代数知识,应该知道几何平移、缩放、旋转变换都是几何变换的一部分,几何变换可以使用线性代数的矩阵来表示,平移对应一个平移矩阵,旋转一个对应旋转矩阵,一个几何体经过了多次旋转、平移等几何变换,每一个变换都有一个对应的矩阵可以表示,所有几何变换对应矩阵的乘积就是一个复合矩阵,可以称为模型矩阵modelMatrix

着色器使用模型矩阵

使用ShaderMaterial编写着色器代码的时候,模型矩阵modelMatrix不用程序员手动声明,Three.js渲染器
系统渲染的时候会自动往ShaderMaterial顶点着色器字符串中插入一句uniform mat4 modelMatrix;

<script id="vertexShader" type="x-shader/x-vertex">
  // uniform mat4 modelMatrix;//不需要声明
  void main(){
    // 模型矩阵modelMatrix对顶点位置坐标进行模型变换
    gl_Position = modelMatrix*vec4( position, 1.0 );
  }
</script>

modelMatrix变量数据传递

查看网格模型Mesh的基类Object3D可知道网格模型有一个本地矩阵属性.matrix.matrix的属性值是一个Three.js的矩阵对象 Matrix4。对网格模型进行平移、旋转、缩放等几何变换都会改变网格模型本地矩阵属性.matrix的属性值。

mesh.rotateY(Math.PI/6);
mesh.rotateX(Math.PI/6);

如果网格模型mesh有一个父对象,父对象的几何变换同样会传递到网格模型,也就是说顶点着色器中默认的模型矩阵变量modelMatrix对应的不是网格模型自身的几何变换,而是网格模型的自身以及它所有父对象的几何变换,一个网格模型自身以及父对象所有的几何变换,会体现在自己的世界矩阵属性.matrixWorld上。

一个网格模型mesh都包含一个几何体Geometry,一个几何体中有一系列的顶点位置数据,这些顶点位置数据需要传递给着色器中顶点位置变量position,同样着色器中uniform关键字声明的模型矩阵变量modelMatrix也需要传递矩阵数据。

Three.js渲染器渲染的时候会自动从一个Threejs的模型对象提取它世界矩阵属性.matrixWorld的属性值,然后传递给着色器的模型矩阵变量modelMatrix,这个过程不需要程序员设置,Three.js系统会自动完成。如果你编写WebGL原生代码都知道需要调用WebGL的相关API完成数据的传递过程,比较麻烦,对于开发者来说不太友好,为了开发者更好的编写着色器代码,Three.js引擎封装了这些WebGL API。

视图矩阵和投影矩阵

相机对象本质上就是存储视图矩阵和投影矩阵的信息的一个对象,基类Camera.matrixWorldInverse属性对应的就是着色器中视图矩阵变量viewMatrix,基类Camera的投影矩阵属性.projectionMatrix对应着色器中的投影矩阵变量projectionMatrix

使用ShaderMaterial构造函数自定义顶点着色器的时候,视图矩阵viewMatrix和投影矩阵projectionMatrix一样不需要手动声明,WebGL渲染器会通过WebGLProgram.js模块自动声明这两个变量,在顶点着色器代码中插入uniform mat4 viewMatrix;uniform mat4 projectionMatrix;

Three.js渲染器执行renderer.render(scene, camera)的时候,会解析相机对象的信息,把相机的矩阵数据自动传递给着色器中的视图矩阵变量viewMatrix和投影矩阵变量projectionMatrix

<script id="vertexShader" type="x-shader/x-vertex">
  void main(){
    gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4( position, 1.0 );
  }
</script>

模型矩阵和视图矩阵构成的复合矩阵称为模型视图矩阵,简称模视矩阵modelViewMatrix,模视矩阵和模型、视图、投影矩阵的使用方式是一样,系统会在着色器代码自动声明该变量,同时把相关的矩阵数据传递给该变量。

<script id="vertexShader" type="x-shader/x-vertex">
  void main(){
    gl_Position = projectionMatrix*modelViewMatrix*vec4( position, 1.0 );
  }
</script>

WebGL坐标系

在原生WebGL编程的时候,WebGL坐标系的z轴垂直canvas画布,x和y轴分别对应于canvas画布的水平和竖直方向,你可以发现能够显示在canvas画布上的顶点坐标范围是[-1,1],如果顶点的xyz某个分量上的坐标值不在-1~1区间内会被剪裁掉不显示。

平时编写Three.js应用程序程序,默认情况下,在Three.js系统中一个模型对应的顶点要经过模型、视图和投影变换后才会在canvas画布上显示出来,如果一个顶点的坐标向量经过一系列的矩阵变换后超出了[-1,1]范围,就不会显示在canvas画布上,平时编程的时候,你可以能会遇到相机参数设置不合适看不到场景中模型的情况,因为视图、投影矩阵的值是由相机的具体参数决定的,相机参数不合适,视图、投影矩阵就会对模型进行不合理的缩放和偏移,导致canvas画布上看不到场景中的模型。


着色器——uniform

使用着色器语言GLSL编写着色器代码的时候,都会使用关键字attributeuniform来声明一些变量,通常关键字attribute用来声明一些几何体顶点数据,例如顶点位置数据、顶点法向量数据…,uniform关键字通常用来声明模型矩阵、光源颜色、光源位置等变量。

  • attribute:属性
  • uniform:统一

Three.js渲染器渲染场景的时候,几何体的顶点位置、颜色、法向量等数据,系统会自动传递给着色器中attribute关键字声明的对应顶点变量。

着色器中uniform关键字声明的模型矩阵modelMatrix、视图矩阵viewMatrix、投影矩阵projectionMatrix等Three.js系统定义的uniform变量,Threejs系统会自动从对应的Threejs对象中解析数据并自动传递。比如视图矩阵的值Three.js系统会从相机对象中获得具体的值,然后传递给viewMatrix变量。

自定义uniform变量数据传递

如果程序员在着色器中任意命名自定义了一个uniform变量,如果需要给该uniform变量传递数据,在原生WebGL中需要特定的WebGL API来传递数据,在Three.js中不需要这样,只需要在着色器材质对象的ShaderMaterial的属性.uniforms中定义一个属性,属性名字和着色器中uniform变量保持一致,对于程序员而言只需要保持名字一致,至于数据传递过程,Three.js系统会自动帮你完成。

属性.uniforms使用案例

片元着色器中通过uniform关键字声明了一个颜色变量color,为了给该变量传递数据在ShaderMaterial对象的uniforms属性中定义了一个名为color的属性,按照Three.js系统uniform变量数据自动传递的机制,如果你在着色器代码中自定义声明了多个uniform变量,只要名字和ShaderMaterial对象中uniform数据的名字保持一直就可以正确完成数据传递。

<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
  // color变量数据来自ShaderMaterial的uniforms属性的color属性
  uniform vec3 color;
  void main() {
     // gl_FragColor = vec4(1.0,0.0,0.0,1.0);
     gl_FragColor = vec4(color,1.0);
  }
</script>

ShaderMaterial的uniforms属性代码

var material = new THREE.ShaderMaterial({
  //定义uniforms属性,uniforms的属性和着色器中的uniform变量相对应
  uniforms:{
    // 颜色属性clor对应片元着色器代码中uniform声明的color变量
    color:{value:new THREE.Color(0xff0000)}
  },
  // 顶点着色器
  vertexShader: document.getElementById('vertexShader').textContent,
  // 片元着色器
  fragmentShader: document.getElementById('fragmentShader').textContent,
});

数据类型

着色器声明的uniform变量数据类型要和着色器材质对象的ShaderMaterial的属性.uniforms的属性的属性值数据类型保持一致。例如value:new THREE.Color(0xff0000)对应的着色器中数据类型是vec3,value:new THREE.Matrix4()对应的着色器中数据类型是mat4

value的值和和着色器数据类型的对应关系可以参考Threejs文档core分类下的Uniform


Three.js着色器——光照计算

为了更好的渲染效果,一般都会对网格模型进行光照计算,光照计算的相关算法是对生活中光线漫反射、镜面反射等光学现象的模拟,如果你不了解光照计算的一些算法可以去学习一下原生的WebGL教程和图形学方面的知识。

前面说过Three.js的材质对象本质上都是着色器代码,有些材质支持光照计算,有些材质不支持光照计算,比如基础网格材质MeshBasicMaterial,有些材质支持光照计算,支持光照计算的材质具体的算法也不尽相同,兰伯特网格材质MeshPhongMaterial、高光网格材质MeshPhongMaterial、标准网格材质MeshStandardMaterial

平行光模型

本节课通过一个平行光的案例来进一步让大家认识着色器材质对象ShaderMaterial的使用。

顶点着色器

系统自动声明的变量

使用ShaderMaterial构造函数自定义着色器的时候,顶点法向量变量normal和顶点位置变量’position’一样不用手动声明,Three.js渲染器系统会通过WebGLPrograms.js模块自动声明attribute vec3 normal;

  • geometry.attributes.position对应着色器中position变量
  • geometry.attributes.normal对应着色器中normal变量
var geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5);
// 查看几何体的顶点位置、顶点法向量数据
console.log(geometry.attributes);

法向量矩阵normalMatrix和模型矩阵modelMatrix一样不需要手动声明系统会自动声明,可以直接在main函数中使用。顶点的位置进行了旋转等变换,顶点的法向量方向肯定会发生变化,所以需要一个法向量矩阵对顶点法向量进行变换,Threejs渲染器模块会根据顶点位置的相关变换矩阵计算normalMatrix的值。

<script id="vertexShader" type="x-shader/x-vertex">
  //varying声明顶点法向量插值后变量
  varying vec3 v_normal;
  void main(){
    // normalMatrix法向量矩阵:模型的顶点进行了模型变换,顶点的法向量要跟着变化
    // 顶点的法向量执行插值计算
    v_normal=normalMatrix*normal;
    // 模型矩阵modelMatrix
    gl_Position = modelMatrix*vec4( position, 1.0 );
  }
</script>

片元着色器

片元着色器代码中包含了平行光漫反射计算的光照模型算法。光线的入射角不同反射的强度不同,一个立方的表面法线方向不相同,与平行光的夹角不同,每个面的明暗就不同。关于光照模型更多的知识可以学习原生WebGL教程和图形学。

<script id="fragmentShader" type="x-shader/x-fragment">
  // 声明一个颜色变量表示网格模型颜色
  uniform vec3 u_color;
  // 顶点法向量插值后的结果,一个片元数据对应一个法向量数据
  varying vec3 v_normal;
  // uniform声明平行光颜色变量
  uniform vec3 u_lightColor;
  //平行光方向变量
  uniform vec3 u_lightDirection;
  void main() {
    // 法向量归一化
    vec3 norlmal2 = normalize(v_normal);
    // 计算平行光方向向量和片元法向量的点积
    // 不同的入射角度反射强度不同
    float dot = dot(u_lightDirection, norlmal2);
    // 计算反射后的颜色  光线颜色*物体颜色*dot
    vec3 reflectedLight = u_lightColor * u_color * dot;
    // 反射颜色赋值给内置变量gl_FragColor
    gl_FragColor = vec4(reflectedLight,1.0);
  }
</script>

uniforms定义

// 定义材质对象的uniforms属性,传递着色器中uniform变量对应的值
uniforms: {
  // 网格模型颜色
  u_color: {
    value: new THREE.Color(0xff0000)
  },
  // 平行光光源颜色
  u_lightColor: {
    value: new THREE.Color(0xffffff)
  },
  // 平行光的方向
  u_lightDirection: {
    value: new THREE.Vector3(-1.0, -1.0, 1.0).normalize()
  },
},

着色器——颜色插值计算

本节课通过一个颜色插值的案例,来加强大家对Three.js着色器代码的理解。

几何体数据

创建一个缓冲类型的几何体BufferGeometry,然后给该几何体Geometry对象设置顶点位置数据和顶点颜色数据,顶点颜色和顶点位置。

把几何体作为网格模型Mesh的参数,也就是使用三角形模式渲染几何体,6个顶点构成2个三角形,每个三角形有三个顶点,每个顶点有一个颜色,三角形中间的颜色是三个顶点颜色插值计算的结果,整个三角形会显示为彩色。

几何体顶点位置数据

如果不是特殊需求,实际开发中一般也不用程序员自定义顶点位置数据,通常通过3D美术软件创建,这里为了大家更容易深度理解Three.js底层知识,自定义顶点位置数据。

//类型数组创建顶点位置position数据
var vertices = new Float32Array([
  0, 0, 0, //顶点1坐标
  50, 0, 0, //顶点2坐标
  0, 100, 0, //顶点3坐标

  0, 0, 10, //顶点4坐标
  0, 0, 100, //顶点5坐标
  50, 0, 10, //顶点6坐标
]);
// 创建属性缓冲区对象
var attribue = new THREE.BufferAttribute(vertices, 3); //3个为一组,作为一个顶点的xyz坐标
// 设置几何体attributes属性的位置position属性
geometry.attributes.position = attribue;
几何体顶点颜色数据

顶点颜色数据和顶点位置数据一一对应

//类型数组创建顶点颜色color数据
var colors = new Float32Array([
  1, 0, 0, //顶点1颜色
  0, 1, 0, //顶点2颜色
  0, 0, 1, //顶点3颜色

  1, 1, 0, //顶点4颜色
  0, 1, 1, //顶点5颜色
  1, 0, 1, //顶点6颜色
]);
// 设置几何体attributes属性的颜色color属性
//3个为一组,表示一个顶点的颜色数据RGB
geometry.attributes.color = new THREE.BufferAttribute(colors, 3);

材质对象vertexColors属性

材质对象Material是自定义材质对象ShaderMaterial的基类,自然ShaderMaterial会继承基类MaterialvertexColors属性和side属性。

前面课程讲到过Three.js的点材质、线材质和网格材质的颜色默认是由color属性值决定的,如果希望Three.js渲染器系统使用几何体的顶点颜色数据进行渲染,需要设置vertexColors: THREE.VertexColors,,对于自定义材质对象ShaderMaterial同样需要这样设置。

var material = new THREE.ShaderMaterial({
  // 顶点着色器
  vertexShader: document.getElementById('vertexShader').textContent,
  // 片元着色器
  fragmentShader: document.getElementById('fragmentShader').textContent,
  //以顶点颜色为准进行渲染
  vertexColors: THREE.VertexColors,
  // 双面可见
  side:THREE.DoubleSide,
});

着色器

顶点着色器

使用ShaderMaterial API的时候顶点颜色变量和顶点位置变量一样不需要手动声明,系统会自动声明attribute vec3 color;

插值计算的实现,要通过着色器语言的关键字varying实现。

// attribute vec3 position;
// attribute vec3 color;

// varying关键字声明一个变量表示顶点颜色插值后的结果
varying vec3 vColor;
void main(){
  // 顶点颜色数据进行插值计算
  vColor = color;
  // 投影矩阵projectionMatrix、视图矩阵viewMatrix、模型矩阵modelMatrix
  gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4( position, 1.0 );
}
片元着色器
// 顶点片元化后有多少个片元就有多少个颜色数据vColor
varying vec3 vColor;
void main() {
  //把插值后的到颜色数据赋值给对应的片元
   gl_FragColor = vec4(vColor,1.0);
}

着色器——纹理贴图

Three.js网格材质都有一个map属性,该属性用来设置网格模型的颜色贴图,渲染器系统会调用网格材质对应的着色器代码解析map属性的值进行渲染。本节课通过自定义着色器的纹理贴图代码来展示网格材质map属性对应的着色器原理。

顶点纹理坐标数据uv

通过Three.js的球体、矩形平面、立方体等特定几何体构造函数创建一个几何体对象,构造函数会按照特定的算法生成顶点位置position、顶点法向量normal、顶点纹理坐标uv数据。

//球体
var geometry = new THREE.SphereBufferGeometry(60, 25, 25);
// 查看几何体的顶点数据
console.log(geometry.attributes);
// 顶点纹理坐标attributes.uv的itemSize属性值是2,意味着顶点纹理坐标是二维向量
// 查看几何体顶点纹理坐标数据uv
console.log(geometry.attributes.uv);

纹理贴图

ShaderMaterial中设置一个属性来表示纹理贴图,对应着色器中texture变量。

uniforms: {
  // texture对应顶点着色器中uniform声明的texture变量
  texture: {
    // 加载纹理贴图返回Texture对象作为texture的值
    // Texture对象对应着色器中sampler2D数据类型变量
    value: new THREE.TextureLoader().load('./Earth.png')
  },
},

着色器

编写着色器代码从纹理贴图上采集像素值然后赋值给片元。

顶点着色器

使用ShaderMaterial API的时候顶点纹理坐标变量uv和顶点位置变量position一样不需要手动声明,系统会自动声明attribute vec2 uv;

// varying关键字声明一个变量表示顶点纹理坐标插值后的结果
varying vec2 vUv;
void main(){
  // 顶点纹理坐标uv数据进行插值计算
  vUv = uv;
  // 投影矩阵projectionMatrix、视图矩阵viewMatrix、模型矩阵modelMatrix
  gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4( position, 1.0 );
}
片元着色器

uniform关键字声明一个数据类型为sampler2D的变量texture,对应uniforms中texture的值。

// 声明一个纹理对象变量
uniform sampler2D texture;
// 顶点片元化后有多少个片元就有多少个纹理坐标数据vUv
varying vec2 vUv;
void main() {
  //内置函数texture2D通过纹理坐标vUv获得贴图texture的像素值
   gl_FragColor = texture2D( texture, vUv );
}

着色器——UV动画

通过自定着色器代码的方式实现UV动画。

Texture偏移属性offset实现UV动画

.wrapS定义了纹理如何水平包裹,并对应于UV映射中的U.

.wrapT这定义了纹理垂直包裹的方式,与UV映射中的V相对应.

var texture = textureLoader.load('./大气.png');
// 设置重复的作用是:能够让一个效果循环
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;

渲染函数周期性执行的过程中,Three.js纹理对象Texture的偏移属性offset两个分量x和y递增或递减。

// 渲染函数
function render() {
  // 每次渲染对纹理对象进行偏移,不停的偏移纹理,就产生了动画的效果
  texture.offset.x -= 0.001;
  texture.offset.y += 0.001;
  group.rotateY(-0.005)
  renderer.render(scene, camera); //执行渲染操作
  requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}

着色器中uniform变量更新

片元着色器中声明的一个时间变量time

// 声明一个时间变量用来控制UV动画
uniform float time;
// 声明一个纹理对象变量
uniform sampler2D texture;
// 顶点片元化后有多少个片元就有多少个纹理坐标数据vUv
varying vec2 vUv;
void main() {
  vec2 newT= vUv + vec2( -0.02, 0.02 ) * time;
  //通过偏移后的纹理坐标newT采样像素
  gl_FragColor = texture2D( texture, newT );
  // 大气层整体透明度增加
  gl_FragColor.a *=0.6;
}
uniforms: {
  // 对应片元着色器中的时间变量time
  time: {
    value: 0.0
  },
},

在渲染函数中不停地更新ShaderMaterial对象uniforms属性的时间变量time的值,每次执行新的渲染,Threejs系统会自动更新片元着色器中的时间变量time的值。

// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
  // 获得两次渲染的时间间隔deltaTime
  var deltaTime = clock.getDelta();

  // 更新uniforms中时间,这样就可以更新着色器中time变量的值
  material.uniforms.time.value += deltaTime;

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

着色器——着色器模块.glsl调用

路径three.js-master\src\renderers\shaders下,ShaderChunk文件中有大量具有特定功能的着色器代码块.glsl,ShaderLib文件夹下面的着色器文件是对ShaderChunk文件中的着色器代码块进行调用组合得到一个新的着色器代码,新的着色器文件是一个完整的顶点着色器或片元着色器代码这些完成的顶点或片元着色器代码和Three.js的点材质、线线材质或网格材质是一一对应的。比如顶点着色器文件meshphong_vert.glsl和片元着色器文件meshphong_frag.glsl对应的是高光网格材质MeshPhongMaterial,顶点着色器文件points_vert.glsl和片元着色器文件points_frag.glsl对应的是点材质PointsMaterial

使用ShaderMaterial自定义着色器代码的时候,可以手动编写着色器代码,也可以调用ShaderChunk文件和ShaderLib文件夹下面的着色器代码模块。

如果想很好的复用three.js的着色器代码块,至少应该阅读下着色器源码,对每一个文件有一个大致的认识。

顶点位置矩阵变换

手动编写顶点位置position进行投影矩阵、相机矩阵、视图矩阵变换的着色器代码。

void main(){
  // 投影矩阵projectionMatrix、视图矩阵viewMatrix、模型矩阵modelMatrix
  gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4( position, 1.0 );

  // modelViewMatrix等价于viewMatrix*modelMatrix
  // gl_Position = projectionMatrix*modelViewMatrix*vec4( position, 1.0 );
}
</script>
调用project_vertex.glsl文件

调用ShaderChunk文件夹下的project_vertex.glsl文件,注意该着色器块文件中的代码依赖着色器文件begin_vertex.glsl。这里也给大家提醒,ShaderChunk文件夹下的着色器模块之间既有一定的独立性,有些着色器代码块有依赖别的着色器代码块。如果想更好的使用这些着色器代码块或者理解Three.js系统原理,阅读每一句着色器代码的工作肯定是要做的。

void main(){
  //模块功能:拷贝顶点位置变量值
  #include <begin_vertex>

  // 模块功能:投影视图模型矩阵变换
  #include <project_vertex>
}
begin_vertex.glsl
//拷贝顶点位置变量值
vec3 transformed = vec3( position );
project_vertex.glsl
// 模型视图矩阵对顶点位置数据进行变换
// modelViewMatrix:模型视图矩阵,模型矩阵和视图矩阵的复合矩阵
vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );
// projectionMatrix:相机的投影矩阵
gl_Position = projectionMatrix * mvPosition;
map_pars_fragment.glsl

使用仅仅使用该模块,注意设置预定义#define USE_MAP;

#ifdef USE_MAP
// 直接声明一个纹理贴图变量
	uniform sampler2D map;

#endif
uv_pars_fragment.glsl
// 如果使用了任何纹理贴图,就需要进行纹理坐标的插值计算,也就是说需要使用varying关键字声明变量vUv
#if defined( USE_MAP )  || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) ...
	// 片元着色器中:声明一个变量vUv用于插值计算
	varying vec2 vUv;

#endif

着色器——系统uniforms模块调用UniformsLib

本节课用到两个APITHREE.UniformsUtilsTHREE.UniformsLib,关于这两个API的使用,官方文档并没有详细介绍,如果想了解它们的使用规则,建议听课简单了解,或者阅读相应的源码,这两个API的所在的文件目录是\three.js-master\src\renderers\shadersTHREE.UniformsUtils提供了一个方法.merge()可以组合THREE.UniformsLib提供的uniforms代码块或者自定义的uniforms属性。

THREE.UniformsLib

访问uniforms代码common块,THREE.UniformsLib["common"]或者THREE.UniformsLib.common访问方式都可以。执行后获得相应的值,可以作为.merge()参数数组的元素。

{
  // 对应材质对象的颜色color属性
  diffuse: { value: new Color( 0xeeeeee ) },
  // 透明度变量
  opacity: { value: 1.0 },
  // 颜色贴图变量
  map: { value: null },
  uvTransform: { value: new Matrix3() },

  alphaMap: { value: null },

}

或者

方法.merge()

方法THREE.UniformsUtils.merge()的作用是拷贝组合THREE.UniformsLib调用的模块代码。
.merge()的参数是一个数组,数组的元素满足格式:

// 自定义uniform变量属性写在一个对象中
  {
    time: {
      value: 0.3
    },
    opacity: {
      value: 0.6
    },
  }

THREE.UniformsLib代码块可以和自定义的uniform属性组合使用。

uniforms: THREE.UniformsUtils.merge([
  THREE.UniformsLib["common"],
  THREE.UniformsLib["fog"],
  {
    // 自定义uniform变量写在一个对象中
    time: {
      value: 0.3
    },
    opacity: {
      value: 0.6
    },
  }
]),

着色器——模仿系统的材质对象

MeshPhongMaterialPointsMaterial等three.js的材质材质对象本质上都是着色器代码,本节课就通过自定义着色器ShaderMaterial调用Threejs系统的着色器库和uniforms库来模仿这些材质对象。

UniformsLib.js

包含一些常见uniform变量对应的属性和属性值

THREE.UniformsLib.common,
THREE.UniformsLib.specularmap,
THREE.UniformsLib.envmap,
...

UniformsUtils.js

方法.merge()用来赋值组合UniformsLib.js中提供的一些uniforms块。

uniforms: THREE.UniformsUtils.merge([
  THREE.UniformsLib.points,
  THREE.UniformsLib.fog,
]),

ShaderChunk.js

通过THREE.ShaderChunk可以获得ShaderChunk和ShaderLib文件下面的所有着色器文件.glsl代码。
比如THREE.ShaderChunk.points_vert返回ShaderLib文件下points_vert.glsl文件中着色器代码字符串,该着色器代码是点材质对象PointsMaterial的顶点着色器;比如THREE.ShaderChunk.meshphong_frag返回ShaderLib文件下meshphong_frag.glsl文件中着色器代码字符串,该着色器代码是高光网格材质对象MeshPhongMaterial的片元着色器;

ShaderLib.js

该代码模块设置了每一种Three.js材质对象的顶点着色器、片元着色器和uniform变量对应的属性和值。

模仿PointsMaterial

var material = new THREE.ShaderMaterial({
  // 调用UniformsLib.js文件中的uniform变量代码块
  uniforms: THREE.UniformsUtils.merge([
    THREE.UniformsLib.points,
    THREE.UniformsLib.fog,
  ]),
  // 获得ShaderLib文件下着色器文件points_vert.glsl代码
  vertexShader: THREE.ShaderChunk.points_vert,
  // 获得ShaderLib文件下着色器文件points_frag.glsl代码
  fragmentShader: THREE.ShaderChunk.points_frag
});

// 重置uniform变量对应属性的值
// 点尺寸设置
material.uniforms.size.value=20.0;
// 点颜色数据设置
material.uniforms.diffuse.value.setRGB(1,0,0)

模仿MeshPhongMaterial

var material = new THREE.ShaderMaterial({
  // 调用UniformsLib.js文件中的uniform变量代码块
  uniforms: THREE.UniformsUtils.merge([
  THREE.UniformsLib.common,
  THREE.UniformsLib.specularmap,
  THREE.UniformsLib.envmap,
  THREE.UniformsLib.aomap,
  THREE.UniformsLib.lightmap,
  THREE.UniformsLib.emissivemap,
  THREE.UniformsLib.bumpmap,
  THREE.UniformsLib.normalmap,
  THREE.UniformsLib.displacementmap,
  THREE.UniformsLib.gradientmap,
  THREE.UniformsLib.fog,
  THREE.UniformsLib.lights,
  {
    emissive: { value: new THREE.Color( 0x000000 ) },
    specular: { value: new THREE.Color( 0x111111 ) },
    shininess: { value: 30 }
  }
  ]),
  // 获得ShaderLib文件下着色器文件meshphong_vert.glsl代码
  vertexShader: THREE.ShaderChunk.meshphong_vert,
  // 获得ShaderLib文件下着色器文件meshphong_frag.glsl代码
  fragmentShader: THREE.ShaderChunk.meshphong_frag
});

// phong受光照影响,ShaderMaterial的lights属性需要设置为true,默认是false
material.lights = true;
// 设置材质颜色
material.uniforms.diffuse.value.setRGB(1.0, 1.0, 0.0)

着色器——自动提取光源对象信息

Three.js有点光源、环境光等等各种常见光源对象,一个应用中会有多个光源对象,在渲染的过程中,Threejs渲染器会自动从这些光源对象提取光源的颜色、位置等信息传值给着色器中的uniform变量。

本节课通过自定义着色器材质ShaderMaterialAPI来演示光源对象的自动化传值过程。

着色器

片元着色器中声明点光源、平行光源、方向光源等等光源对应的所有uniform变量。

// 大多数着色器模块依赖该模块,该模块定义了大多数通用的常量、变量和函数
#include <common>
// 光照计算的一些相关算法函数
#include <bsdfs>
// 声明点光源、环境光、方向光等等光源的uniform变量
// <lights_pars_begin>模块依赖<bsdfs>和<common>
#include <lights_pars_begin>

访问光源信息

lights_pars_begin中声明一些光源相关的变量,比如一个光源对象的所有属性对应一个自定义结构体,所有方向光光源对象作为着色器中一个数组变量的元素。

访问第一个方向光源的方向directionalLights[0].direction,访问第二个方向光光源的颜色directionalLights[1].color

uniforms属性

调用THREE.UniformsLib["lights"]设置ShaderMaterial材质对象的uniforms属性。该属性设置后,光源对象相关的值value都是空的,只要设置material.lights = true;,这些光源对象相关的uniform变量对应的值value,Threejs渲染器系统会自动帮你从Threejs光源对象中提取相关的信息。具体的提取过程可以阅读three.js-master\src\renderers\webgl目录下WebGLLights.js等源码模块了解。


着色器——phong网格材质二次开发

通过着色器材质ShaderMaterial编写着色器代码自定义一个材质对象,保证材质对象实现高光网格材质MeshPhongMaterial的功能,同时增加灰度计算的功能。

实现思路

完全重新编写ShaderMaterial的着色器代码比较麻烦,可以复制Three.js高光网格材质MeshPhongMaterial对应的顶点着色器代码meshphong_vert.glsl和片元着色器代码meshphong_frag.glsl,然后在复制的代码基础上进行改写。灰度计算代码属于片元着色器代码,所以只需要修改片元着色器代码meshphong_frag.glsl即可。

var material = new THREE.ShaderMaterial({
  // 通过THREE.ShaderLib获得MeshPhongMaterial材质对象的uniforms值
  // 用于给着色器中的uniform变量传值
  uniforms: THREE.ShaderLib['phong'].uniforms,
  // 顶点着色器
  vertexShader: THREE.ShaderChunk['meshphong_vert'],
  // 片元着色器
  fragmentShader: document.getElementById('fragmentShader').textContent,
});
meshphong_frag.glsl修改

复制meshphong_frag.glsl着色器代码,然后增加一个灰度计算的功能

<script id="fragmentShader" type="x-shader/x-fragment">
#define PHONG
uniform vec3 diffuse;
...
uniform float opacity;
#include <common>
...
...
#include <envmap_fragment>
// 原来的给片元赋值的代码注释,重新编写加入灰度计算功能
// gl_FragColor = vec4( outgoingLight, diffuseColor.a );

//计算RGB三个分量光能量之和,也就是亮度
float luminance = 0.299*outgoingLight.r+0.587*outgoingLight.g+0.114*outgoingLight.b;
//逐片元赋值,RGB相同均为亮度值,用黑白两色表达图片的明暗变化
gl_FragColor = vec4(luminance,luminance,luminance,diffuseColor.a);

...
...
</script>

WebGLRenderTarget(离屏渲染)

WebGL渲染目标对象WebGLRenderTarget实现了WebGL的离屏渲染功能,如果你有一定的WebGL或OpenGL基础,对帧缓冲区、离线渲染、后处理等概念应该是不陌生的。

.render()方法

WebGL渲染器WebGLRenderer渲染方法.render()的参数( Scene, Camera, WebGLRenderTarget, forceClear ).

  • Scene:要渲染的场景对象
  • Camera:场景对象对应的相机对象
  • WebGLRenderTarget:如果参数指定了WebGL渲染目标WebGLRenderTarget,渲染的图像结果保存到该对象,或者说保存到GPU自定义帧缓冲区中,不会显示到canvas画布上; 如果没有指定渲染目标,也就是没有该参数,渲染结果会直接显示到canvas画布上,或者说渲染结果保存到canvas画布对应的默认帧缓冲区中.

无渲染目标(Canvas显示)

执行下面代码会把场景scene的渲染结果保存到canvas画布对应的默认帧缓冲区中,形象点说就是可以直接显示到Cnavas画布上,显示器会自动读取CPU默认帧缓冲区上面的图像数据显示。

  renderer.render(scene, camera);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
// 渲染结果canvas元素插入到body元素中
document.body.appendChild(renderer.domElement);
// .domElement属性返回的一个canvas画布对象,保存了render方法的渲染结果
console.log(renderer.domElement);

有渲染目标(Canvas不显示)

执行下面代码WebGL渲染器的渲染结果,也就是一张图像,不会直接显示在Canvas画布上,从Three.js的角度阐述,渲染结果的RGBA像素数据存储到了WebGL渲染目标对象WebGLRenderTarget中,通过目标对象的纹理属性.texture可以获得渲染结果的RGBA像素数据,也就是一个Three.js的纹理对象THREE.Texture,可以作为材质对象颜色贴图属性map的属性值;从原生WebGL的角度阐述,就是渲染结果的RGBA像素值存储到了GPU一个自定义的帧缓冲区中,屏幕默认不会直接读取该缓冲区中的像素数据,通过WebGL的特定API可以获取,更多的信息可以百度WebGL或OpenGL离屏渲染。

// 创建一个WebGL渲染目标对象WebGLRenderTarget
// 设置渲染结果(一帧图像)的像素为500x500
var target = new THREE.WebGLRenderTarget(500, 500);
// 设置特定target的时候,render渲染结果不会显示在canvas画布上
renderer.render(scene, camera,target); //执行渲染操作

.texture

通过WebGL渲染目标WebGLRenderTarget的纹理属性.texture可以获得WebGL渲染器的渲染结果,该属性返回的结果是一个纹理对象THREE.Texture,可以作为材质Material对象颜色贴图属性map的属性。

var material = new THREE.MeshLambertMaterial({
  // WebGL渲染目标对象属性.texture返回一张纹理贴图,也就是scene在camera下的渲染结果
  map: target.texture,
});

WebGLRenderTarget实现灰度图后处理功能

这节课主要内容是把WebGL渲染目标对象WebGLRenderTarget和自定义着色器材质对象ShaderMaterial结合实现后处理功能。

灰度计算后处理

场景Scene对象的渲染结果保存到渲染目标对象target中

var target = new THREE.WebGLRenderTarget(500, 500);

renderer.render(scene, camera,target);

target.texture从渲染目标对象target获得渲染结果,然后通过ShaderMaterial对象把渲染结果传值给片元着色器中uniform定义的变量texture,然后进行灰度计算后处理。

// 自定义顶点着色器对象
var material2 = new THREE.ShaderMaterial({
  uniforms: {
    // texture对应顶点着色器中uniform声明的texture变量
    texture: {
      // WebGL渲染目标对象属性.texture返回一张纹理贴图
      value: target.texture
    },
  },
  // 顶点着色器
  vertexShader: document.getElementById('vertexShader').textContent,
  // 片元着色器
  fragmentShader: document.getElementById('fragmentShader').textContent,
});

材质对象material2是场景2中一个网格模型的纹理贴图,通过render渲染方法把后处理灰度效果显示出来

renderer.render(scene2, camera2);

创建多个WebGL渲染目标对象

可以创建多个WebGL渲染目标对象分别保存一个WebGL渲染器的渲染结果,满足一个应用需要在GPU上临时保存多个后处理效果,而不显示在Canvas画布上。

// 创建一个WebGL渲染目标对象target0,像素500X500
var target0 = new THREE.WebGLRenderTarget(500, 500);
// 创建一个WebGL渲染目标对象target1,像素300X500
var target1 = new THREE.WebGLRenderTarget(500, 500);

后处理EffectComposer—自定义着色器

3D场景和相机设置好后,执行渲染器渲染相机下的场景,渲染结果就是一张图片,周期性地执行渲染器渲染方法,一帧一帧图片就构成了动画效果。
后处理简单的说,可以理解为处理图片,比如把一张彩色图变成灰度图,或者给一张图片或者图片场景中一个物体添加一个一个边框…

EffectComposer.js封装了WebGL渲染目标WebGLRenderTargetAPI,相比直接使用WebGLRenderTarget进行后处理要方便得多。

后期处理相关.js文件路径

后处理相关的库基本都在路径three.js-master\examples\js\下面postprocessingshaders
两个文件夹下。

EffectComposer.js库依赖RenderPass.js、ShaderPass.js、CopyShader.js。

<!-- 引入EffectComposer.js库  封装了WebGLRenderTarget  可以调用WebGL渲染器的渲染方法 -->
<script src="./three.js-master/examples/js/postprocessing/EffectComposer.js"></script>

<!-- renderPass.js库  构造函数传入场景Scene和相机Camera作为构造函数renderPass的参数 -->
<script src="./three.js-master/examples/js/postprocessing/RenderPass.js"></script>

<!-- 这两个好像不能删除   EffectComposer依赖它们-->
<!-- ShaderPass.js库,一个ShaderPass调用一个自定义着色器代码就构成一个后处理通道 -->
<script src="./three.js-master/examples/js/postprocessing/ShaderPass.js"></script>

<!-- 引入CopyShader.js库  CopyShader.js包含着色器代码,着色器代码功能:采样一张图片像素赋值给片元 -->
<script src="./three.js-master/examples/js/shaders/CopyShader.js"></script>

EffectComposer.js

EffectComposer构造函数的参数是渲染器对象renderer.

var composer = new THREE.EffectComposer(renderer);

EffectComposer方法.render()

EffectComposer构造函数的参数是WebGL渲染器,执行EffectComposer的渲染方法.render()方法,相当于执行了WebGL渲染器对象的.render()方法。

function render() {
  // EffectComposer的渲染方法.render()执行一次,相当于执行一次renderer.render()得到一帧图像
  composer.render();
  requestAnimationFrame(render);
}

EffectComposer方法.addPass()

该方法用于给EffectComposer对象添加后处理通道,可以添加多个后处理通道,每个通道就是一个处理环节,通道本质就是着色器代码。

// 把渲染器作为参数
var composer = new THREE.EffectComposer(renderer);
// 设置renderPass通道,该通道并不对渲染结果的像素数据进行处理
composer.addPass(renderPass);
// 设置灰度图通道grayShaderPass,对渲染结果进行灰度计算处理
composer.addPass(grayShaderPass);

通道对象的属性.renderToScreen

默认值是false,经过该通道的处理后的图像结果保存到EffectComposer对象的WebGL渲染目标对象WebGLRenderTarget中,如果你有WebGL基础,你也可以理解为把结果保存到自定义的帧缓冲区中,不会在canvas画布上直接显示。

如果设置Pass.renderToScreen = true;,表示经过该通道的处理结果存储到系统默认的帧缓冲区中,也就是直接显示在canvas画布上面。

RenderPass通道

RenderPass构造函数的参数是场景和相机对象(scene,camera) ,RenderPass通道的作用是把场景和相机作为参数传入,获得场景的渲染结果,并不对渲染结果做特定处理。如果EffectComposer对象只使用该通道,可以简单认为和直接调用WebGL渲染器的render方法区别不大,最终效果是一样的。一般来说RenderPass通道是EffectComposer对象的第一个通道。

var renderPass = new THREE.RenderPass(scene, camera);
// 渲染结果默认不显示,如果renderToScreen设置为true,经过该通道处理后会直接显示到Caanvas画布上
renderPass.renderToScreen = true;

var composer = new THREE.EffectComposer(renderer);
// 渲染通道插入EffectComposer对象中
composer.addPass(renderPass);

THREE.ShaderPass通道

该通道是着色器通道,可以自定义后处理的着色器代码作为THREE.ShaderPasss构造函数的参数。

顶点着色器和片元着色器的编写要遵守一定的格式,具体格式可以参照CopyShader.js文件,在该文件的基础上进行修改,CopyShader.js文件中的着色器代码基本功能就是获取颜色贴图的像素值赋值给片元,不做特定功能的后期处理。

下面顶点和片元着色器代码的后期处理功能就是灰度计算。

顶点着色器代码和CopyShader.js文件中顶点着色器代码一样没有改变。

<script id="vertexShader" type="x-shader/x-vertex">
  // 声明一个变量vUv表示uv坐标插值后的结果
  varying vec2 vUv;
  void main(){
    // 纹理坐标插值计算
    vUv = uv;
    // projectionMatrix投影矩阵  modelViewMatrix模型视图矩阵
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  }
</script>

片元着色器在CopyShader.js片元着色器的基础上进行了一定更改,插入一段灰度计算的代码。

<script id="fragmentShader" type="x-shader/x-fragment">
  // 默认设置颜色贴图的变量是tDiffuse
  uniform sampler2D tDiffuse;
  varying vec2 vUv;
  void main() {
    //采集纹素
    vec4 tColor = texture2D( tDiffuse, vUv );
    //计算RGB三个分量光能量之和,也就是亮度
    float luminance = 0.299*tColor.r+0.587*tColor.g+0.114*tColor.b;
    //逐片元赋值,RGB相同均为亮度值,用黑白两色表达图片的明暗变化
    gl_FragColor = vec4(luminance,luminance,luminance,1);
  }
</script>

创建着色器通道ShaderPass,着色器通道ShaderPass构造函数参数格式和着色器材质ShaderMaterial构造函数的选项参数一样。

//自定义后处理通道
var GreyShader = {
  uniforms: {
    // 和着色器tDiffuse变量对应
    // THREE.ShaderPass会把渲染结果,也就是一张图片的像素值对应Texture对象赋值给tDiffuse
    tDiffuse: {
      value: null
    },
  },
  vertexShader: document.getElementById('vertexShader').textContent,
  fragmentShader: document.getElementById('fragmentShader').textContent,
}
// GreyShader作为THREE.ShaderPass的参数
var grayShaderPass = new THREE.ShaderPass(GreyShader);

自定义R分量提取功能的着色器代码

直接修改片元着色器就可以

void main() {
  gl_FragColor = texture2D( tDiffuse, vUv );
}
void main() {
  //采集纹素
  vec4 tColor = texture2D( tDiffuse, vUv );
  //计算RGB三个分量光能量之和,也就是亮度
  float luminance = 0.299*tColor.r+0.587*tColor.g+0.114*tColor.b;
  //逐片元赋值,RGB相同均为亮度值,用黑白两色表达图片的明暗变化
  gl_FragColor = vec4(luminance,luminance,luminance,1);
}
void main() {
  //采集纹素
  vec4 tColor = texture2D( tDiffuse, vUv );
  //逐片元赋值,RGB相同均为亮度值,用黑白两色表达图片的明暗变化
  gl_FragColor = vec4(tColor.r,0,0,1);
}

多个处理通道

多个通道之间是串联关系,执行一个通道的渲染结果,默认保存得到CPU自定义帧缓冲区中,不会显示在Canvas画布上,如果某个通道设置Pass.renderToScreen = true;,渲染结果就会直接显示在Canvas画布上。

var composer = new THREE.EffectComposer(renderer);
// 设置renderPass通道
composer.addPass(renderPass);
// 设置R分量提取通道RShaderPass
composer.addPass(RShaderPass);
// 设置灰度图通道grayShaderPass,对渲染结果进行灰度计算处理
composer.addPass(grayShaderPass);


后处理EffectComposer——直接调用常见通道

上节课讲解的是自定义通道的着色器代码,本节课讲解直接调用一个特定功能的通道模块,通道使用的着色器代码已经配置好,不需要自己编写。

GlitchPass通道

效果:随机产生电脉冲

GlitchPass通道依赖THREE.DigitalGlitch提供的uniforms对象、顶点着色器代码和片元着色器代码。
THREE.DigitalGlitch的路径three.js-master\examples\js\shaders\DigitalGlitch.js

var renderPass = new THREE.RenderPass(scene, camera);
var GlitchPass = new THREE.GlitchPass(64);
GlitchPass.renderToScreen = true;
var composer = new THREE.EffectComposer(renderer);
composer.addPass(renderPass);
composer.addPass(GlitchPass);

FilmPass通道

模拟电视屏效果

var renderPass = new THREE.RenderPass(scene, camera);
var FilmPass = new THREE.FilmPass(0.3, 0.4, 512, false);
FilmPass.renderToScreen = true;
var composer = new THREE.EffectComposer(renderer);
composer.addPass(renderPass);
composer.addPass(FilmPass);

OutlinePass通道

一个模型外面添加一个高亮的外边框

var renderPass = new THREE.RenderPass(scene, camera);
var OutlinePass = new THREE.OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
OutlinePass.renderToScreen = true;
var composer = new THREE.EffectComposer(renderer);
composer.addPass(renderPass);
composer.addPass(OutlinePass);

//设置需要添加外边框的网格模型
//交互的时候可以设置一个鼠标事件,点击选中了某个模型,就直接把某个网格模型作为值的元素
OutlinePass.selectedObjects = [mesh];
11-08 10:22