1. 概述

我在《WebGL简易教程(五):图形变换(模型、视图、投影变换)》这篇博文里详细讲解了OpenGL\WebGL关于绘制场景的图形变换过程,并推导了相应的模型变换矩阵、视图变换矩阵以及投影变换矩阵。这里我就通过three.js这个图形引擎,验证一下其推导是否正确,顺便学习下three.js是如何进行图形变换的。

2. 基本变换

2.1. 矩阵运算

three.js已经提供了向量类和矩阵类,定义并且查看一个4阶矩阵类:

var m = new THREE.Matrix4();
m.set(11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44);
console.log(m);

输出结果:

说明THREE.Matrix4内部是列主序存储的,而我们理论描述的矩阵都为行主序。

2.2. 模型变换矩阵

在场景中新建一个平面:

// create the ground plane
var planeGeometry = new THREE.PlaneGeometry(60, 20);
var planeMaterial = new THREE.MeshBasicMaterial({
color: 0xAAAAAA
});
var plane = new THREE.Mesh(planeGeometry, planeMaterial); // add the plane to the scene
scene.add(plane);

three.js中场景节点的基类都是Object3D,Object3D包含了3种矩阵对象:

  1. Object3D.matrix: 相对于其父对象的局部模型变换矩阵。
  2. Object3D.matrixWorld: 对象的全局模型变换矩阵。如果对象没有父对象,则与Object3D.matrix相同。
  3. Object3D.modelViewMatrix: 表示对象相对于相机坐标系的变换。也就是matrixWorld左乘相机的matrixWorldInverse。

2.2.1. 平移矩阵

平移这个mesh:

plane.position.set(15, 8, -10);

根据推导得到平移矩阵为:

\[\left[
\begin{matrix}
1 & 0 & 0 & Tx\\
0 & 1 & 0 & Ty\\
0 & 0 & 1 & Tz\\
0 & 0 & 0 & 1
\end{matrix}
\right]
\]

输出这个Mesh:

2.2.2. 旋转矩阵

2.2.2.1. 绕X轴旋转矩阵

绕X轴旋转:

plane.rotation.x = THREE.Math.degToRad(30);

对应的旋转矩阵:

\[\left[
\begin{matrix}
1 & 0 & 0 & 0\\
0 & cosβ & -sinβ & 0\\
0 & sinβ & cosβ & 0\\
0 & 0 & 0 & 1
\end{matrix}
\right]
\]

输出信息:

2.2.2.2. 绕Y轴旋转矩阵

绕Y轴旋转:

plane.rotation.y = THREE.Math.degToRad(30);

对应的旋转矩阵:

\[\left[
\begin{matrix}
cosβ & 0 & sinβ & 0\\
0 & 1 & 0 & 0\\
-sinβ & 0 & cosβ & 0\\
0 & 0 & 0 & 1
\end{matrix}
\right]
\]

输出信息:

2.2.2.3. 绕Z轴旋转矩阵

绕Z轴旋转:

plane.rotation.z = THREE.Math.degToRad(30);

对应的旋转矩阵:

\[\left[
\begin{matrix}
cosβ & -sinβ & 0 & 0\\
sinβ & cosβ & 0 & 0\\
0 & 0 & 1 & 0\\
0 & 0 & 0 & 1
\end{matrix}
\right]
\]

输出信息:

2.3. 投影变换矩阵

在场景中新建一个Camera:

var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

这里创建了一个透视投影的相机,一般建立的都是对称的透视投影,推导的透视投影矩阵为:

\[P=
\left[
\begin{matrix}
\frac{1}{aspect*tan⁡(\frac{fovy}{2})} & 0 & 0 & 0 \\
0 & \frac{1}{tan⁡(\frac{fovy}{2})} & 0 & 0 \\
0 & 0 & \frac{f+n}{n-f} & \frac{2fn}{n-f} \\
0 & 0 & -1 & 0 \\
\end{matrix}
\right]
\]

为了验证其推导是否正确,输出这个camera,查看projectionMatrix,也就是透视投影矩阵:

2.4. 视图变换矩阵

通过Camera可以设置视图矩阵:

camera.position.set(0, 0, 100);   //相机的位置
camera.up.set(0, 1, 0); //相机以哪个方向为上方
camera.lookAt(new THREE.Vector3(1, 2, 3)); //相机看向哪个坐标

根据《WebGL简易教程(五):图形变换(模型、视图、投影变换)》中的描述,可以通过three.js的矩阵运算来推导其视图矩阵:

var eye = new THREE.Vector3(0, 0, 100);
var up = new THREE.Vector3(0, 1, 0);
var at = new THREE.Vector3(1, 2, 3); var N = new THREE.Vector3();
N.subVectors(eye, at);
N.normalize();
var U = new THREE.Vector3();
U.crossVectors(up, N);
U.normalize();
var V = new THREE.Vector3();
V.crossVectors(N, U);
V.normalize(); var R = new THREE.Matrix4();
R.set(U.x, U.y, U.z, 0,
V.x, V.y, V.z, 0,
N.x, N.y, N.z, 0,
0, 0, 0, 1); var T = new THREE.Matrix4();
T.set(1, 0, 0, -eye.x,
0, 1, 0, -eye.y,
0, 0, 1, -eye.z,
0, 0, 0, 1); var V = new THREE.Matrix4();
V.multiplyMatrices(R, T);
console.log(V);

其推导公式如下:

\[V=R^{-1} T^{-1}=
\left[
\begin{matrix}
Ux & Uy & Uz & 0 \\
Vx & Vy & Vz & 0 \\
Nx & Ny & Nz & 0 \\
0 & 0 & 0 & 1 \\
\end{matrix}
\right] *
\left[
\begin{matrix}
1 & 0 & 0 & -Tx \\
0 & 1 & 0 & -Ty\\
0 & 0 & 1 & -Tz\\
0 & 0 & 0 & 1\\
\end{matrix}
\right] =
\left[
\begin{matrix}
Ux & Uy & Uz & -U·T \\
Vx & Vy & Vz & -V·T \\
Nx & Ny & Nz & -N·T \\
0 & 0 & 0 & 1 \\
\end{matrix}
\right]
\]

最后输出它们的矩阵值:



两者的计算结果基本时一致的。需要注意的是Camera中表达视图矩阵的成员变量是Camera.matrixWorldInverse。它的逻辑应该是视图矩阵与模型矩阵互为逆矩阵,模型矩阵也可以称为世界矩阵,那么世界矩阵的逆矩阵就是视图矩阵了。

3. 着色器变换

可以通过给着色器传值来验证计算的模型视图投影矩阵(以下称MVP矩阵)是否正确。对于一个任何事情都不做的着色器来说:

vertexShader: `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`
, fragmentShader: `
void main() {
gl_FragColor = vec4(0.556, 0.0, 0.0, 1.0)
}`

projectionMatrix和modelViewMatrix分别是three.js中内置的投影矩阵和模型视图矩阵。那么可以做一个简单的验证工作,将计算得到的MVP矩阵传入到着色器中,代替这两个矩阵,如果最终得到的值是正确的,那么就说明计算的MVP矩阵是正确的。

3.1. 代码

实例代码如下:

<!DOCTYPE html>
<html> <head>
<title>Example 01.01 - Basic skeleton</title>
<meta charset="UTF-8" />
<script type="text/javascript" charset="UTF-8" src="../three/three.js"></script>
<script type="text/javascript" charset="UTF-8" src="../three/controls/TrackballControls.js"></script>
<script type="text/javascript" charset="UTF-8" src="../three/libs/stats.min.js"></script>
<script type="text/javascript" charset="UTF-8" src="../three/libs/util.js"></script>
<script type="text/javascript" src="MatrixDemo.js"></script>
<link rel="stylesheet" href="../css/default.css">
</head> <body>
<!-- Div which will hold the Output -->
<div id="webgl-output"></div> <!-- Javascript code that runs our Three.js examples -->
<script type="text/javascript">
(function () {
// contains the code for the example
init();
})();
</script>
</body> </html>
'use strict';

THREE.StretchShader = {

    uniforms: {
"sw" : {type:'b', value : false},
"mvpMatrix" : {type:'m4',value:new THREE.Matrix4()}
}, //
vertexShader: `
uniform mat4 mvpMatrix;
uniform bool sw;
void main() {
if(sw) {
gl_Position = mvpMatrix * vec4( position, 1.0 );
}else{
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
}`
, //
fragmentShader: `
uniform bool sw;
void main() {
if(sw) {
gl_FragColor = vec4(0.556, 0.0, 0.0, 1.0);
}else {
gl_FragColor = vec4(0.556, 0.8945, 0.9296, 1.0);
}
}`
}; function init() {
//console.log("Using Three.js version: " + THREE.REVISION); // create a scene, that will hold all our elements such as objects, cameras and lights.
var scene = new THREE.Scene(); // create a camera, which defines where we're looking at.
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); // position and point the camera to the center of the scene
camera.position.set(0, 0, 100); //相机的位置
camera.up.set(0, 1, 0); //相机以哪个方向为上方
camera.lookAt(new THREE.Vector3(1, 2, 3)); //相机看向哪个坐标 // create a render and set the size
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(new THREE.Color(0x000000));
renderer.setSize(window.innerWidth, window.innerHeight); // add the output of the renderer to the html element
document.getElementById("webgl-output").appendChild(renderer.domElement); // create the ground plane
var planeGeometry = new THREE.PlaneGeometry(60, 20);
// var planeMaterial = new THREE.MeshBasicMaterial({
// color: 0xAAAAAA
// }); var planeMaterial = new THREE.ShaderMaterial({
uniforms: THREE.StretchShader.uniforms,
vertexShader: THREE.StretchShader.vertexShader,
fragmentShader: THREE.StretchShader.fragmentShader
}); var plane = new THREE.Mesh(planeGeometry, planeMaterial); // add the plane to the scene
scene.add(plane); // rotate and position the plane
plane.position.set(15, 8, -10);
plane.rotation.x = THREE.Math.degToRad(30);
plane.rotation.y = THREE.Math.degToRad(45);
plane.rotation.z = THREE.Math.degToRad(60); render(); var farmeCount = 0;
function render() { var mvpMatrix = new THREE.Matrix4();
mvpMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
mvpMatrix.multiplyMatrices(mvpMatrix, plane.matrixWorld); THREE.StretchShader.uniforms.mvpMatrix.value = mvpMatrix;
if(farmeCount % 60 === 0){
THREE.StretchShader.uniforms.sw.value = !THREE.StretchShader.uniforms.sw.value;
} farmeCount = requestAnimationFrame(render);
renderer.render(scene, camera);
} }

3.2. 解析

这段代码的意思是,给着色器传入了计算好的MVP矩阵变量mvpMatrix,以及一个开关变量sw。开关变量会每60帧变一次,如果为假,会使用内置的projectionMatrix和modelViewMatrix来计算顶点值,此时场景中的物体颜色会显示为蓝色;如果开关变量为真,则会使用传入的计算好的mvpMatrix计算顶点值,此时场景中的物体颜色会显示为红色。运行截图如下:

可以看到场景中的物体的颜色在红色与蓝色之间来回切换,且物体位置没有任何变化,说明我们计算的MVP矩阵是正确的。

4. 其他

在使用JS的console.log()进行打印camera对象的时候,会发现如果不调用render()的话(或者单步调式),其内部的matrix相关的成员变量仍然是初始化的值,得不到想要的结果。而console.log()可以认为是异步的,调用render()之后,就可以得到正确的camera对象了。

three.js中的矩阵变换(模型视图投影变换)的更多相关文章

  1. WebGL简易教程(六):第一个三维示例(使用模型视图投影变换)

    目录 1. 概述 2. 示例:绘制多个三角形 2.1. Triangle_MVPMatrix.html 2.2. Triangle_MVPMatrix.js 2.2.1. 数据加入Z值 2.2.2. ...

  2. js中的盒子模型

    说到盒子模型,你第一时间会想到css盒子模型,css中的盒子模型包括(内容区+内边距+边框).那在js中怎么去获取这些属性值呢?下面一起来学习js中的盒子模型. css样式 body { margin ...

  3. WebGL或OpenGL关于模型视图投影变换的设置技巧

    目录 1. 具体实例 2. 解决方案 1) Cube.html 2) Cube.js 3) 运行结果 3. 详细讲解 1) 模型变换 2) 视图变换 3) 投影变换 4) 模型视图投影矩阵 4. 存在 ...

  4. Java基础之在窗口中绘图——使用模型/视图体系结构在视图中绘图(Sketcher 1 drawing a 3D rectangle)

    控制台程序. 在模型中表示数据视图的类用来显示草图并处理用户的交互操作,所以这种类把显示方法和草图控制器合并在一起.不专用于某个视图的通用GUI创建和操作在SketcherFrame类中处理. 模型对 ...

  5. 【带着canvas去流浪(14)】Three.js中凹浮雕模型的生成方式

    目录 一. 方案1:ThreeBSP.js或ThreeCSG.js扩展库 二. 方案2:平面镂空模型拉伸 三. 方案3:Cinema 4D建模后输出模型文件 示例代码托管在:http://www.gi ...

  6. OpenGL模型视图变换、投影变换、视口变换的理解

    OpenGL中不设置模型,投影,视口,所绘制的几何图形的坐标只能是-1到1(X轴向右,Y轴向上,Z轴垂直屏幕向外). 产生目标场景的过程类似于用照相机进行拍照: (1)把照相机固定在三角架上,并让他对 ...

  7. JS中的函数、Bom、DOM及JS事件

    本期博主给大家带来JS的函数.Bom.DOM操作,以及JS各种常用的数据类型的相关知识,同时,这也是JavaScript极其重要的部分,博主将详细介绍各种属性的用法和方法. 一.JS中的函数 [函数的 ...

  8. 从零开始的JS生活(二)——BOM、DOM与JS中的事件

    上回书说道,JS中变量.运算符.分支结构.循环和嵌套循环等内容.本回就由本K给大伙唠唠JS中的BOM.DOM和事件. 一."花心大萝卜"--BOM 1.震惊,FFF团为何对BOM举 ...

  9. angular.js 中同步视图和模型数据双向绑定,$watch $digest $apply 机制

    Angular.js 中的特性,双向绑定. 让视图的改变直接反应到数据中,数据的改变又实时的通知到视图,如何做到的? 这要归功于 scope 下面3个重要的方法: $watch $digest $ap ...

随机推荐

  1. [ddt02篇]十年测试老鸟帮您解析:ddt结合txt,excel,csv,mysql实现自动化测试数据驱动

    一.前言: 阅读此文之前请先阅读: [ddt01篇]十年测试老鸟帮您解析:ddt数据驱动入门基础应用:https://www.cnblogs.com/csmashang/p/12679448.html ...

  2. Anaconda下的juputer notebook 更改起始目录的方法【填坑】

    出来的结果是这样的,我们很不习惯,找文件.保存文件很麻烦 这里的快捷方式可以打开 jupyter notebook ,但是如果你没配置环境变量的话,在cmd 中 输入命令 jupyter notebo ...

  3. SPFA()判环

    1 SPFA()判负环 SPFA()判负环的原理就是在求最短路的过程中,如果存在负环,比如说要求从A到a的最短距离,设为s,但是经过a->c->b->a可以更短,所以如果一直经过a- ...

  4. E - Max Sum Plus Plus Plus HDU - 1244 (线性区间DP)

    题目大意:  值得注意的一点是题目要求的是这些子段之间的最大整数和.注意和Max Sum Plus Plus这个题目的区别. 题解: 线性区间DP,对每一段考虑取或者不取.定义状态dp[i][j]指的 ...

  5. Gatling 条件判断

    在使用Gatling的过程中,当前置接口异常,无法获取到数据作为其他接口的请求参数室,接口是不能请求的.或者通过feeder获取的数据要区分不同的情况请求不同的接口.此时,使用gatling的判断语句 ...

  6. HBase Filter 过滤器概述

    abc 过滤器介绍 HBase过滤器是一套为完成一些较高级的需求所提供的API接口. 过滤器也被称为下推判断器(push-down predicates),支持把数据过滤标准从客户端下推到服务器,带有 ...

  7. RocketMQ存储机制与确认重传机制

    引子 消息队列之前就听说过,但一直没有学习和接触,直到最近的工作流引擎项目用到,需要了解学习一下.本文主要从一个初学者的角度针对RocketMQ的存储机制和确认重传机制做一个浅显的总结. 存储机制 我 ...

  8. Java IO 流-- 文件拷贝

    IO流操作套路: 1.创建源: 2.选择流: 3.操作: 4.释放资源 上代码: package com.xzlf.io; import java.io.File; import java.io.Fi ...

  9. 天池Docker学习赛笔记

    容器的基本概念 什么是容器? 容器就是一个视图隔离.资源可限制.独立文件系统的进程集合.所谓"视图隔离"就是能够看到部分进程以及具有独立的主机名等:控制资源使用率则是可以对于内存大 ...

  10. PHP--关于上传文件大小的问题

    参考:https://www.cnblogs.com/jianqingwang/p/5863960.html https://blog.csdn.net/u013168253/article/deta ...