三维场景在我们的工作和生活中非常常见,比如百度地图,电影,游戏等等。
我们最熟悉的就是我们的定位地图,在我们的定位系统中,可以使用三维场景作为地图显示使用,同时能通过鼠标操作视角,观看场景中的不同位置,以及我们可以把定位卡坐标通过三维模型(行走的小人)显示到场景中。
目前我们实现三维场景使用的是threeJs,这是一个基于webGL的3D库。
先说一下webGL,webGL是一种3D绘图协议,为html5 canvas提供硬件3D加速渲染, 让开发人员就可以借助显卡在浏览器里更流畅地展示3D场景和模型(摘抄自百度) 。
webGL的技术规范继承自免费开源的openGL标准,后者在计算机图形学,电子游戏,设计等领域广泛使用。可以说webGL就是web版的openGL。
threeJS在webGL的基础上定义了一系列常用的API,让开发者可以非常简单方便的搭建三维场景。
import * as THREE from './lib/three.module.js';
import Stats from './lib/stats.module.js';
import {OrbitControls} from './lib/OrbitControls.js';
let container, renderer, camera, scene, stats ;
init();
animate();
function init () {
// 获取dom,<div id="container"></div
container = document.getElementById('container');
// 新建渲染器,并加入到dom中
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
// 新建透视摄像机,参数为(视角,宽高比,近剪裁平面,远剪裁平面)
camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 5000);
camera.position.y = 100;
camera.position.z = 20;
// 新建场景,场景的作用是一个容器,模型,灯光等等都会放在场景中
scene = new THREE.Scene();
scene.background = new THREE.Color(0xbfd1e5);
// 新建一个用于显示实时帧率的组件
stats = new Stats();
container.appendChild(stats.dom);
// 摄像机控制组件,用来操作摄像机
const controls = new OrbitControls(camera, renderer.domElement);
}
function animate () {
requestAnimationFrame(animate);
render();
stats.update();
}
function render () {
renderer.render(scene, camera);
}
以上代码,搭建了一个基础的三维场景,其中包含了一些必要的元素,渲染器、像机、场景、以及代码中没有提到的模型、材质、贴图等,这些元素不仅仅存在threeJs的世界里,其他三维引擎(unity3D,虚幻4)中也是通过这些元素搭建三维世界的。
下面就详细说说这些元素的作用。
渲染从字面意思上理解,就是把数据转化成图像的过程,比如前端把html代码渲染成为网页。
上面的代码代码中,我新建了一个渲染器加入到场景中。
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth,window.innerHeight);
container.appendChild(renderer.domElement);
渲染器的作用是把三维场景中的模型渲染成为二维画面展示给用户。如果不经过渲染,我们看到的三维世界就是一大堆代码。
"geometries": [
{
"uuid": "A046F00F-06A1-4E33-B28C-0F25D7FEEB7A",
"type": "BufferGeometry",
"data": {
"attributes": {
"position": {
"itemSize": 3,
"type": "Float32Array",
"array": [-0.5,-0.5,0.5,0.5,-0.5,0.5,0.5,0.5,0.5,-0.5,-0.5,0.5,0.5,0.5,0.5,-0.5,0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,-0.5,-0.5,0.5,0.5,0.5,0.5,-0.5,-0.5,0.5,-0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,-0.5,-0.5,0.5,-0.5,0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5,0.5,-0.5,0.5,-0.5,-0.5,-0.5,0.5,-0.5,0.5,-0.5,-0.5,0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,-0.5,0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5],
"normalized": false
},
"normal": {
"itemSize": 3,
"type": "Float32Array",
"array": [-1,0,0,-1,0,0,-1,0,0,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0],
"normalized": false
},
"uv": {
"itemSize": 2,
"type": "Float32Array",
"array": [0.375,0,0.625,0,0.625,0.25,0.375,0,0.625,0.25,0.375,0.25,0.375,0.25,0.625,0.25,0.625,0.5,0.375,0.25,0.625,0.5,0.375,0.5,0.375,0.5,0.625,0.5,0.625,0.75,0.375,0.5,0.625,0.75,0.375,0.75,0.375,0.75,0.625,0.75,0.625,1,0.375,0.75,0.625,1,0.375,1,0.625,0,0.875,0,0.875,0.25,0.625,0,0.875,0.25,0.625,0.25,0.125,0,0.375,0,0.375,0.25,0.125,0,0.375,0.25,0.125,0.25],
"normalized": false
}
},
"boundingSphere": {
"center": [0,0,0],
"radius": 0.866025
}
}
}],
上面的代码无法一眼就看出所描述的是一个正方体,通过渲染我们才可以直观的看到它得样子。
提到渲染器,从事三维工作的人员,一般从事三维工作的人员都会想到vray(主要用于室内设计),menterRay,anorld(主要用于影视动画)等,这些渲染器都能渲染出难辨真伪的画面。
这些渲染器为了追求真实的画面效果,会进行非常多的的运算,比如光线的反射,折射,阴影的处理,材质等等,运算量非常大,一般完成一副画面的渲染都需要很长时间。
除去这些渲染器,一般用于交互的三维场景(三维软件的主界面,三维地图,游戏,虚拟城市等),使用的都是实时渲染器,实时渲染器优化(简化)渲染的过程,把没有经过太多运算的画面展示给用户,方便用户操作。
(以上图片不仅仅来源于网络)
虽然现在很多游戏画面效果非常真实,不过在渲染的开销远远没有影视渲染器那么多,一般采用次世代技术(使用法线贴图,光照贴图等)达到真实的效果。
我们的定位地图场景中使用了灯光贴图,让光线看起来比较自然舒服,实际上场景中的物体不受灯光影响,完全是贴图在发挥作用。
摄像机的责任是告诉渲染器,需要渲染场景中那些物体,更准确的说法是规定渲染的范围,进入摄像机范围的模型才会被渲染。
比如这个场景,并没有把场景中的所有物体都渲染出来,仅仅渲染了进入摄像机范围的部分。
以上场景使用的是透视摄像机,这种摄像机模拟现实中的摄像机设计,摄像机的范围看起是一个四棱锥,越接近摄像机,范围也就越小,物体也就会被渲染得越大,也就实现了近大远小。
在之前的threeJs代码中,加入摄像机的代码中有4个参数,视角,宽高比,近剪裁平面和远剪裁平面。视角就是视线的夹角,宽高比就是屏幕的宽高比(设置错误会造成画面拉伸),近剪裁平面以及远剪裁平面用于规定摄像机可视范围。
// 新建透视摄像机,参数为(视角,宽高比,近剪裁平面,远剪裁平面)
camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 5000);
camera.position.y = 100;
camera.position.z = 20;
实际上在threeJs中,透视摄像机的可视范围是一个梯形,如下图,处在可视范围外的物体或者部分都会被剪裁掉(不渲染)。
下图展示了设置近剪裁平面的效果,图中得立方体一个角被剪裁掉了。
除了透视摄像机,还有一种摄像机叫正交摄像机,现实中基本不存在这种摄像机,按道理说,如果现实摄像机非常遥远,就可以非常接近正交相机的效果,比如卫星图。
卫星离地面非常遥远,拍摄的图片看起来就没有什么透视,图中(百度卫星地图)大楼的顶部和底部都是差不多的大小,非常接近正交视图的画面。
正交相机的范围从形状上看,是一个长方体,无论物体距离摄像机多远多近,渲染出来的大小都不会改变,正交相机视图会让人觉得非常奇怪(我们看到的世界都是带有透视的),一般会用在场景制作过程中,以及一些上帝视角的二维游戏中(比如传奇)。
这里先说一下三维坐标系的问题,threeJs采用的笛卡尔坐标系,这是一种非常通用的坐标系。与我们熟悉的坐标系有一些不同,实际上我们通过简单的算法,转变了笛卡尔坐标系,以方便与二维地图对应。
下图中可以看出,我们的Z轴就是笛卡尔的Y轴,而我们的Y轴是笛卡尔的-Z轴。
这里我们不关心二维地图对应的问题,使用通用的笛卡尔坐标系完成接下来的事情。
之前我们已经设置好了渲染器,摄像机已经场景,这是一个空的场景,场景中什么都没有,我们只能看到背景色,下面我们需要把模型加入到场景中。
实际使用中,我们一般会使用三维制作软件(maya,max,blender等)制作模型,然后导入到我们的场景中,比如我们的定位场景模型,定位卡标签模型等。有时候我们还需要通过数据直接生成模型,比如我们场景中显示的电子围栏。
要在三维场景中绘制电子围栏,需要理解模型的几何结构,几何结构描述模型的形状和位置,构成结构最基本的元素是顶点( vector )。
几何结构描述模型的形状和位置,构成结构最基本的元素是顶点( vector )。
每个顶点都是由一组三维坐标构成(x,y,z),这个坐标描绘了顶点在三维空间中的位置,三个顶点可以构成一个三角面,三维世界中的一切形体都可以用三角面还塑造,不管建筑还是生物。
我们先看看一个正方体在三维空间中,每个顶点的位置正方体上面有8个顶点,每一个顶点都有一组三维坐标。正方体有6个面,每个面都是由两个三边面构成。
我们从最简单的三角形开始,threeJs提供了bufferGeometry和geometry两种方式绘制三角形,geometry的方式是bufferGeometry的一种友好替代方式,提供了很API方便开发者更加简单方便的创建几何结构,不过效率上比bufferGrometry要差。
下面分别用bufferGeometry和geometry的方式往场景中加入一个三角形。
bufferGeometry:
let geom = new THREE.BufferGeometry();
geom.setAttribute('position',new THREE.BufferAttribute(new Float32Array([
0.0,0.0,0.0,
1.0,0.0,1.0,
1.0,0.0,0.0
]),3));
let material = new THREE.MeshBasicMaterial({color: 0xffdd99});
let triangle = new THREE.Mesh(geom, material);
scene.add(triangle);
geometry:
let geom = new THREE.Geometry();
geom.vertices.push(
new THREE.Vector3(0,0,0),
new THREE.Vector3(1,0,1),
new THREE.Vector3(1,0,0)
);
geom.faces.push(new THREE.Face3(0,1,2));
let material = new THREE.MeshBasicMaterial({color: 0xffdd99});
let triangle = new THREE.Mesh(geom, material);
scene.add(triangle);
创建的图形如下图,从顶点1开始到顶点3结束。仔细思考一下,其实不管从哪个顶点开始,哪个顶点结束,顺序怎么样,都能构成一个三角形才对。
实际上从哪个顶点开始不是关键的,threeJs遵循右手法则,就是下面的图片这样,顶点循序按照逆时针方向加入的话,三角形的面就是向上的,反之则是向下,threeJs默认是单面显示,只会显示正面,不会显示背面。
所以我们加入点的循序可以是123,231,312都可以,要是加入循序为321,那么画面中就不会看到我们创建的三角型。
我们系统中的电子围栏,就可以用这种方式去实现,下图是在我们的系统中绘制的一个电子围栏。
我的想法大致如下图,按照顺序逐一加入顶点。
代码实现如下,电子围栏的顶点坐标放在一个二维数组里面,循环这个数组,按照思路逐一把顶点加入就行了。
const h = 0.5; // 围栏高度
const area = [[0.0,2.0],[0.5,1.0],[2.0,1.0],[1.0,0.0],
[2.0,-1.0],[1.0,-1.0], [0.0,0.0],[-1.0,-1.0],
[-2.0,-1.0],[-1.0,0.0],[-2.0,1.0],[-0.5,1.0]];
const len = area.length;
let position = [];
for(let i = 0; i < len ; i++){
if(i == 0){
position = [...position,area[i][0],0.0,-area[i][1]];
position = [...position,area[i][0],h,-area[i][1]];
}else{
position = [...position,area[i][0],0.0,-area[i][1]];
position = [...position,area[i][0],0.0,-area[i][1]];
position = [...position,area[i-1][0],h,-area[i-1][1]];
position = [...position,area[i][0],h,-area[i][1]];
position = [...position,area[i][0],0.0,-area[i][1]];
position = [...position,area[i][0],h,-area[i][1]];
}
if(i == (len -1 )){
position = [...position,area[0][0],0.0,-area[0][1]];
position = [...position,area[0][0],0.0,-area[0][1]];
position = [...position,area[i][0],h,-area[i][1]];
position = [...position,area[0][0],h,-area[0][1]];
}
}
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position',new THREE.BufferAttribute(new Float32Array(position),3));
const material = new THREE.MeshBasicMaterial({color: 0x8844ff,side:2,transparent:true,opacity:0.5});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
然后就可以在三维场景中看到我们的电子围栏了,这里把围栏的显示设置成了双面,避免有些面我们看不到。
围栏方便演示,上面的代码后方加入再加入一段代码,给这个几何结构指定另一个显示线框的材质球并加入场景中。
const wire = new THREE.MeshBasicMaterial({color: 0x000000,wireframe:true});
const wire_mesh = new THREE.Mesh(geometry, wire);
scene.add(wire_mesh);
如下图,可以看到围栏的构成。
上面的例子中可以看到,在创建了bufferGeometry后又创建一个meshBasicMaterial,这是一个threeJs提供的基本材质。在三维中仅有几何结构(顶点和面的信息)是没办法渲染出三维图形的,还需要着色器才能让渲染器知道应该怎么去描绘图形。
着色器是渲染三维图形的关键,用GLSL ES语言编写,threeJs为开发者封装好了一系列常用的着色器,上面的meshBasicMaterial是其中的一种,threeJs里叫材质,这个材质仅仅不受光照的影响,显示模型的固有颜色和贴图信息。常用的材质还有meshLambertMaterial,meshPhoneMaterial等。
下面我使用可以接收灯光的meshLambertMaterial材质创建一个立方体,并且往场景中加入灯光。
let geom = new THREE.BufferGeometry();
geom.setAttribute('position',new THREE.BufferAttribute(new Float32Array([
0.0,0.0,0.0, 1.0,0.0,0.0, 1.0,0.0,1.0,
0.0,0.0,0.0, 1.0,0.0,1.0, 0.0,0.0,1.0,
0.0,0.0,0.0, 0.0,1.0,0.0, 1.0,0.0,0.0,
0.0,1.0,0.0, 1.0,1.0,0.0, 1.0,0.0,0.0,
0.0,0.0,0.0, 0.0,1.0,1.0, 0.0,1.0,0.0,
0.0,0.0,0.0, 0.0,0.0,1.0, 0.0,1.0,1.0,
0.0,1.0,1.0, 0.0,0.0,1.0, 1.0,1.0,1.0,
0.0,0.0,1.0, 1.0,0.0,1.0, 1.0,1.0,1.0,
1.0,1.0,1.0, 1.0,0.0,1.0, 1.0,1.0,0.0,
1.0,0.0,1.0, 1.0,0.0,0.0, 1.0,1.0,0.0,
0.0,1.0,0.0, 0.0,1.0,1.0, 1.0,1.0,0.0,
0.0,1.0,1.0, 1.0,1.0,1.0, 1.0,1.0,0.0,
]),3));
let material = new THREE.MeshLambertMaterial({color: 0xffdd99});
let box = new THREE.Mesh(geom, material);
scene.add(box);
下面的脚本中加入了三个灯光,两个平行光和一个环境光。
let ambient = new THREE.AmbientLight(0xcccccc);
scene.add(ambient);
let directional = new THREE.DirectionalLight(0xffffff,0.6);
directional.position.x = 3;
directional.position.z = 5;
directional.position.y = 10;
scene.add(directional);
let directional_back = new THREE.DirectionalLight(0xffffff,0.1);
directional_back.position.x = -5;
directional_back.position.z = -3;
directional_back.position.y = 0;
scene.add(directional_back);
渲染结果是下面这样,立方体完全没有受到灯光的影响,一点立体感都没有,看起来就是一个二维得六边形。
为什么会这样呢,原因是我们创建的几何结构缺少normal元素,也就是法线。
法线用来表述表面的方向,一般情况下,法线是垂直与平面的一根线(面法线),三维中面法线依靠组成面的顶点法线来描述(顶点法线)。
法线为面的时候,面上的三个顶点法线都是垂直与面的一根线。
法线是由一组三维坐标组成的一个向量,每个顶点的法线都可以单独设置。
法线决定模型在灯光照耀下的颜色,我们知道现实中的物体都是因为反射光线才能被我们看到的,现实中的物体大多数表面都是粗糙的,反射光以不同的方向射出,我们在不同角度都能看到物体,这种被成为漫反射。如果物体的表面向镜子一样光滑,那么光线就会以特定的方向反射出去。我们通过镜子看到反射的物体,就需要在特定的位置才行。
在三维世界中,漫反射通过材质(material)表现出来。物体的颜色取决与光线颜色,物体的固有色以及光线与面法线的夹角,计算公式为:光的颜色 * 固有色 * cos(夹角)。
使用threeJs的编辑器验证了一下,结果跟我们算出来的一样,可喜可贺。
注意,新版官方的编辑器默认开启伽马矫正,颜色会比实际的亮,使用我们改动后的编辑器可以设置伽马
了解法线概念后,我们继续完成之前的立方体,加入法线。每个面上三个顶点的法线都是一样的,也就是把法线设置成了面法线。
let geom = new THREE.BufferGeometry();
geom.setAttribute('position',new THREE.BufferAttribute(new Float32Array([
0.0,0.0,0.0, 1.0,0.0,0.0, 1.0,0.0,1.0,
0.0,0.0,0.0, 1.0,0.0,1.0, 0.0,0.0,1.0,
0.0,0.0,0.0, 0.0,1.0,0.0, 1.0,0.0,0.0,
0.0,1.0,0.0, 1.0,1.0,0.0, 1.0,0.0,0.0,
0.0,0.0,0.0, 0.0,1.0,1.0, 0.0,1.0,0.0,
0.0,0.0,0.0, 0.0,0.0,1.0, 0.0,1.0,1.0,
0.0,1.0,1.0, 0.0,0.0,1.0, 1.0,1.0,1.0,
0.0,0.0,1.0, 1.0,0.0,1.0, 1.0,1.0,1.0,
1.0,1.0,1.0, 1.0,0.0,1.0, 1.0,1.0,0.0,
1.0,0.0,1.0, 1.0,0.0,0.0, 1.0,1.0,0.0,
0.0,1.0,0.0, 0.0,1.0,1.0, 1.0,1.0,0.0,
0.0,1.0,1.0, 1.0,1.0,1.0, 1.0,1.0,0.0,
]),3));
geom.setAttribute('normal',new THREE.BufferAttribute(new Float32Array([
0.0,-1.0,0.0, 0.0,-1.0,0.0, 0.0,-1.0,0.0,
0.0,-1.0,0.0, 0.0,-1.0,0.0, 0.0,-1.0,0.0,
0.0,0.0,-1.0, 0.0,0.0,-1.0, 0.0,0.0,-1.0,
0.0,0.0,-1.0, 0.0,0.0,-1.0, 0.0,0.0,-1.0,
-1.0,0.0,0.0, -1.0,0.0,0.0, -1.0,0.0,0.0,
-1.0,0.0,0.0, -1.0,0.0,0.0, -1.0,0.0,0.0,
0.0,0.0,1.0, 0.0,0.0,1.0, 0.0,0.0,1.0,
0.0,0.0,1.0, 0.0,0.0,1.0, 0.0,0.0,1.0,
1.0,0.0,0.0, 1.0,0.0,0.0, 1.0,0.0,0.0,
1.0,0.0,0.0, 1.0,0.0,0.0, 1.0,0.0,0.0,
0.0,1.0,0.0, 0.0,1.0,0.0, 0.0,1.0,0.0,
0.0,1.0,0.0, 0.0,1.0,0.0, 0.0,1.0,0.0,
]),3));
let material = new THREE.MeshLambertMaterial({color: 0xffdd99});
let box = new THREE.Mesh(geom, material);
scene.add(box);
结果就是下面的这样,立方体拥有了体感。
上面有说,每个顶点的法线都是可以单独设置的,我们完成的立方体法线是这个样子的,每个面上的三个顶点的法线都垂直于面。
下图三个平面,第一个法线全部朝着一个方向,且与面垂直(面法线);
第二个法线方向一致,不过并不垂直与平面,平面的颜色变暗了,不过整个平面都是一个颜色;
第三个法线方向不一致,平面呈现出平滑过度的两个颜色;
思考一下,为什么要这样设置法线以及为什么三维引擎允许我们可以设置每个顶点的法线,这样做有什么意义呢。
看一下下面两个物体,左边的菱角分明,而右边的却很光滑,两个物体的几何结构是一样的,为何右边的看起来非常光滑呢。
显示两个物体的法线,可以看到左边物体为面法线,也就是单独每个点的法线都是一个方向并且垂直与面。而右边物体的法线并不垂直于平面,并且不是同一个方向,仔细观察可以法线每个顶点的法线于邻面上顶点的法线重合。
光线照射在顶点上的时候根据之前说的算法,可以得到这个顶点的颜色,若是平面上所有顶点的颜色都是一致的(法线方向一致),那么平面也就是这个颜色,两个相邻平面的颜色不一致,我们看到的图像就有了菱角(左图的效果)。
若是平面上顶点的法线方向不一样,也是说每个点的颜色也就不相同,片面就会呈现渐变的多个颜色,当相邻平面的交接处(临边)颜色一致的时候,我们就不会看到一条边了。
这样设置法线的方式在三维世界中非常常见,如果我只是用面法线,那么看到三维世界就会向纸折出来的一样,一点都不真实。
我们在电影游戏中看到的很多武铁表面都是光滑的,正是因为运用了法线的特点,特别在以前的游戏中,电脑性能不能支持一个场景太多面的情况下,一个柱形物体往往只有很少的面数(八边柱),而我们看起来也挺圆的。
几何结构(geometry)除了顶点的位置(position),法线方向(normal)外还有一个属性叫UV,UV描述的是面在二维上的映射位置。
为啥需要把三维的面映射到二维呢?
现实中纸盒子可以帮助我们非常好的去理解UV,下图是一个化妆品的包装盒,图案和文字都被印刷在一张平面的纸上,通过折叠和黏贴,可以得到一个漂亮的纸盒子。
下图是用maya制作的盒子,使用纸盒子的示意图做贴图。通过UV,把二维的图片映射到三维的模型上。
在three.js中,UV由一组二维坐标构成,对应position中的每个三角面,描述模型每个面在UV中的位置。
let geom = new THREE.BufferGeometry();
geom.setAttribute('position',new THREE.BufferAttribute(new Float32Array([
0.0,0.0,0.0, 1.0,0.0,0.0, 1.0,0.0,1.0,
0.0,0.0,0.0, 1.0,0.0,1.0, 0.0,0.0,1.0,
0.0,0.0,0.0, 0.0,1.0,0.0, 1.0,0.0,0.0,
0.0,1.0,0.0, 1.0,1.0,0.0, 1.0,0.0,0.0,
0.0,0.0,0.0, 0.0,1.0,1.0, 0.0,1.0,0.0,
0.0,0.0,0.0, 0.0,0.0,1.0, 0.0,1.0,1.0,
0.0,1.0,1.0, 0.0,0.0,1.0, 1.0,1.0,1.0,
0.0,0.0,1.0, 1.0,0.0,1.0, 1.0,1.0,1.0,
1.0,1.0,1.0, 1.0,0.0,1.0, 1.0,1.0,0.0,
1.0,0.0,1.0, 1.0,0.0,0.0, 1.0,1.0,0.0,
0.0,1.0,0.0, 0.0,1.0,1.0, 1.0,1.0,0.0,
0.0,1.0,1.0, 1.0,1.0,1.0, 1.0,1.0,0.0,
]),3));
geom.setAttribute('normal',new THREE.BufferAttribute(new Float32Array([
0.0,-1.0,0.0, 0.0,-1.0,0.0, 0.0,-1.0,0.0,
0.0,-1.0,0.0, 0.0,-1.0,0.0, 0.0,-1.0,0.0,
0.0,0.0,-1.0, 0.0,0.0,-1.0, 0.0,0.0,-1.0,
0.0,0.0,-1.0, 0.0,0.0,-1.0, 0.0,0.0,-1.0,
-1.0,0.0,0.0, -1.0,0.0,0.0, -1.0,0.0,0.0,
-1.0,0.0,0.0, -1.0,0.0,0.0, -1.0,0.0,0.0,
0.0,0.0,1.0, 0.0,0.0,1.0, 0.0,0.0,1.0,
0.0,0.0,1.0, 0.0,0.0,1.0, 0.0,0.0,1.0,
1.0,0.0,0.0, 1.0,0.0,0.0, 1.0,0.0,0.0,
1.0,0.0,0.0, 1.0,0.0,0.0, 1.0,0.0,0.0,
0.0,1.0,0.0, 0.0,1.0,0.0, 0.0,1.0,0.0,
0.0,1.0,0.0, 0.0,1.0,0.0, 0.0,1.0,0.0,
]),3));
geom.setAttribute('uv', new THREE.BufferAttribute(new Float32Array([
0.0, 0.0, 1.0, 0.0, 1.0, 1.0,
0.0, 0.0, 1.0, 1.0, 0.0, 1.0,
0.0, 0.0, 0.0, 1.0, 1.0, 0.0,
0.0, 1.0, 1.0, 1.0, 1.0, 0.0,
0.0, 0.0, 1.0, 1.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
1.0, 1.0, 0.0, 1.0, 1.0, 0.0,
0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0, 1.0, 0.0,
0.0, 1.0, 1.0, 1.0, 1.0, 0.0,
]), 2));
new THREE.TextureLoader().load('./gold.png', function (texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
let material = new THREE.MeshLambertMaterial({ map: texture });
let box = new THREE.Mesh(geom, material);
scene.add(box);
});
继续之前的立方体,加入UV属性,并且导入一张贴图后的效果就是这样的。
实际模型使用的时候,UV和贴图都要复杂很多,而且贴图也不仅仅只是显示模型的表面纹理,three.js提供了多种贴图用于提升场景效果,比如反射贴图、高光贴图、透明贴图、自发光贴图等等,依赖贴图,可以让并不复杂的模型呈现出复杂的效果,这个技术也就是常说的次世代技术。
下图是仅仅有漫反射贴图的效果,艺术家(浦贵)通过一笔一画绘制的头像,一般被成为手绘贴图,大名鼎鼎的魔兽世界技术就是采用这种制作方式搭建了整个艾泽拉斯。
我们的地图,使用的也是漫反射贴图的方式表现,地图上的光照,颜色阴影都是通过软件计算出来的。这种方式叫做烘培,先渲染出模型每个面的光影和颜色保存到贴图走个,再把贴图贴到模型上,使用这种技术可以弥补实时渲染光照能力不足的缺陷,游戏行业中广泛使用的一种技术。
最后说一下法线贴图,法线用于模型每个面在光照下的颜色显示,同时可以通过设置法线方向改变模型的软硬边。实际上通过贴图也能改变模型表面的法线方向,让单调的平面波澜起伏,并且会随着灯光位置的改变而改变。法线贴图是次世代技术非常重要的组成部分,在不增加模型面数的情况下,让模型的细节更加丰富,更趋近于真实。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。