八、纹理贴图
1. 创建纹理贴图
通过纹理贴图加载器TextureLoader的load()
方法加载一张图片可以返回一个纹理对象Texture,纹理对象Texture
可以作为模型材质颜色贴图.map
属性的值。
材质的颜色贴图属性.map
设置后,模型会从纹理贴图上采集像素值,这时候一般来说不需要再设置材质颜色.color
。.map
贴图之所以称之为颜色贴图就是因为网格模型会获得颜色贴图的颜色值RGB。
// var geometry = new THREE.PlaneGeometry(204, 102); //矩形平面
var geometry = new THREE.SphereGeometry(60, 25, 25); //球体
// TextureLoader创建一个纹理加载器对象,可以加载图片作为几何体纹理
var textureLoader = new THREE.TextureLoader();
// 执行load方法,加载纹理贴图成功后,返回一个纹理对象Texture
textureLoader.load('Earth.png', function(texture) {
var material = new THREE.MeshLambertMaterial({
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture,
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
//纹理贴图加载成功后,调用渲染函数执行渲染操作
// render();
})
纹理对象Texture
通过图片加载器ImageLoader可以加载一张图片,纹理对象Texture的.image
属性值是一张图片。
var geometry = new THREE.SphereGeometry(60, 25, 25); //球体
// 图片加载器
var ImageLoader = new THREE.ImageLoader();
// load方法回调函数,按照路径加载图片,返回一个html的元素img对象
ImageLoader.load('Earth.png', function(img) {
// image对象作为参数,创建一个纹理对象Texture
var texture = new THREE.Texture(img);
// 下次使用纹理时触发更新
texture.needsUpdate = true;
var material = new THREE.MeshLambertMaterial({
map: texture, //设置纹理贴图
});
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
});
总结
- TextureLoader 纹理加载器
- Texture 纹理对象
- image属性:html的img元素
- 图片宽高最好是2的次方,如:512 * 512,64 * 64...
- image属性:html的img元素
- ImageLoader 图片加载器
- 内部使用 FileLoader 文件加载器
- 加载图片返回一个image对象,可以用来生成纹理
- 内部使用 FileLoader 文件加载器
- Texture 纹理对象
2. UV映射原理(顶点纹理坐标)
在课程的第二章对Threejs几何体Geometry和BufferGeometry的顶点概念做过比较多的介绍,讲解过顶点位置坐标数据、顶点颜色数据、顶点法线方向向量数据,不过顶点的UV数据没有去讲解,主要是几何体顶点的纹理坐标数据和纹理贴图的映射有关系,所以放在了本章节去讲解。
几何体纹理坐标数据:
- Geometry:
geometry.faceVertexUvs[0]
- BufferGeometry:
geometry.attributes.uv
纹理映射
纹理UV坐标和顶点位置坐标是一一对应关系,这也就是为什么一张图片可以映射到一个模型的表面,只要把图片的每个纹理坐标和模型的顶点位置建立一对一的关系,就可以实现图像到模型的映射。
纹理UV坐标:图片左下角为坐标原点,右上角为坐标(1, 1),图片上所有位置纵横坐标都介于0.0~1.0之间。
几何体UV坐标
几何体Geometry有两组UV坐标,第一组组用于.map
、.normalMap
、.specularMap
等贴图的映射,第二组用于阴影贴图.lightMap
的映射。。
- 对应Geometry的
Geometry.faceVertexUvs[0]
、Geometry.faceVertexUvs[1]
; - 对应BufferGeometry的
geometry.attributes.uv
、geometry.attributes.uv2
;
修改纹理坐标
- 采样纹理:几何体表面所有位置全部对应贴图(0.4,0.4)坐标位置的像素值,这样话网格模型不会显示完整的地图,而是显示采样点纹理坐标
(0.4,0.4)
对应的RGB值。
var geometry = new THREE.PlaneGeometry(204, 102); //矩形平面
// 查看默认的uv坐标
console.log(geometry.faceVertexUvs[0]);
// 遍历uv坐标
geometry.faceVertexUvs[0].forEach(elem => {
elem.forEach(Vector2 => {
// 所有的UV坐标全部设置为一个值
Vector2.set(0.4,0.4);
});
});
- 局部三角面显示纹理贴图
// 矩形平面 设置细分数4,4
var geometry = new THREE.PlaneGeometry(204, 102, 4, 4);
// 遍历uv坐标
geometry.faceVertexUvs[0].forEach(elem => {
elem.forEach(Vector2 => {
// 所有的UV坐标全部设置为一个值
Vector2.set(0.4,0.4);
});
});
/**
* 局部三角面显示完整纹理贴图
*/
var t0 = new THREE.Vector2(0, 1); //图片左下角
var t1 = new THREE.Vector2(0, 0); //图片右下角
var t2 = new THREE.Vector2(1, 0); //图片右上角
var t3 = new THREE.Vector2(1, 1); //图片左上角
var uv1 = [t0, t1, t3]; //选中图片一个三角区域像素——用于映射到一个三角面
var uv2 = [t1, t2, t3]; //选中图片一个三角区域像素——用于映射到一个三角面
// 设置第五、第六个三角面对应的纹理坐标
geometry.faceVertexUvs[0][4] = uv1
geometry.faceVertexUvs[0][5] = uv2
自定义顶点uv坐标
Geometry
var geometry = new THREE.Geometry(); //创建一个空几何体对象
/**顶点坐标(纹理映射位置)*/
var p1 = new THREE.Vector3(0,0,0); //顶点1坐标
var p2 = new THREE.Vector3(160,0,0); //顶点2坐标
var p3 = new THREE.Vector3(160,80,0); //顶点3坐标
var p4 = new THREE.Vector3(0,80,0); //顶点4坐标
geometry.vertices.push(p1,p2,p3,p4); //顶点坐标添加到geometry对象
/** 三角面1、三角面2*/
var normal = new THREE.Vector3( 0, 0, 1 ); //三角面法向量
var face0 = new THREE.Face3( 0, 1, 2, normal); //三角面1
var face1 = new THREE.Face3( 0, 2, 3, normal); //三角面2
geometry.faces.push( face0,face1 ); //三角面1、2添加到几何体
/**纹理坐标*/
var t0 = new THREE.Vector2(0,0);//图片左下角
var t1 = new THREE.Vector2(1,0);//图片右下角
var t2 = new THREE.Vector2(1,1);//图片右上角
var t3 = new THREE.Vector2(0,1);//图片左上角
uv1 = [t0,t1,t2];//选中图片一个三角区域像素——映射到三角面1
uv2 = [t0,t2,t3];//选中图片一个三角区域像素——映射到三角面2
geometry.faceVertexUvs[0].push(uv1,uv2);//纹理坐标传递给纹理三角面属性
BufferGeometry
var geometry = new THREE.BufferGeometry(); //声明一个空几何体对象
//类型数组创建顶点位置position数据
var vertices = new Float32Array([
0, 0, 0, //顶点1坐标
80, 0, 0, //顶点2坐标
80, 80, 0, //顶点3坐标
0, 80, 0, //顶点4坐标
]);
// 创建属性缓冲区对象
var attribue = new THREE.BufferAttribute(vertices, 3); //3个为一组
// 设置几何体attributes属性的位置position属性
geometry.attributes.position = attribue
var normals = new Float32Array([
0, 0, 1, //顶点1法向量
0, 0, 1, //顶点2法向量
0, 0, 1, //顶点3法向量
0, 0, 1, //顶点4法向量
]);
// 设置几何体attributes属性的位置normal属性
geometry.attributes.normal = new THREE.BufferAttribute(normals, 3); //3个为一组,表示一个顶点的xyz坐标
// Uint16Array类型数组创建顶点索引数据
var indexes = new Uint16Array([
0, 1, 2, 0, 2, 3,
])
// 索引数据赋值给几何体的index属性
geometry.index = new THREE.BufferAttribute(indexes, 1); //1个为一组
/**纹理坐标*/
var uvs = new Float32Array([
0,0, //图片左下角
1,0, //图片右下角
1,1, //图片右上角
0,1, //图片左上角
]);
// 设置几何体attributes属性的位置normal属性
geometry.attributes.uv = new THREE.BufferAttribute(uvs, 2); //2个为一组,表示一个顶点的纹理坐标
效果图:
加载包含uv的外部模型
/**
* 加载外部模型,手动设置材质的纹理
*/
// 创建一个加载threejs格式JSON文件的加载器
var loader = new THREE.ObjectLoader();
// TextureLoader创建一个纹理加载器对象,可以加载图片作为几何体纹理
var textureLoader = new THREE.TextureLoader();
loader.load('model.json',function (obj) {
console.log(obj);
scene.add(obj);//加载返回的对象插入场景中
// 执行load方法,加载纹理贴图成功后,返回一个纹理对象Texture
textureLoader.load('Earth.png', function(texture) {
// 设置球体网格模型材质的map属性
obj.children[0].material.map = texture;
// 告诉threejs渲染器系统,材质对象的map属性已更新
obj.children[0].material.needsUpdate=true;
})
})
3. 数组材质、材质索引materialIndex
数组材质
你可以测试把数组材质作为几何体的纹理贴图,所谓数组材质就是多个材质对象构成一个数组作为模型对象的材质。
所谓数组材质,就是threejs几何体API的算法自动设置系列Face3的材质索引属性
materialIndex
。默认数组材质需要材质对象元素数量:
- 球体、平面:1
- 圆柱体:3
- 立方体:6
- ...
var geometry = new THREE.BoxGeometry(100, 100, 100);
// 材质对象1
var material_1 = new THREE.MeshPhongMaterial({
color: 0xffff3f
})
var textureLoader = new THREE.TextureLoader(); // 纹理加载器
var texture = textureLoader.load('Earth.png'); // 加载图片,返回Texture对象
// 材质对象2
var material_2 = new THREE.MeshLambertMaterial({
map: texture, // 设置纹理贴图
});
// 设置材质数组
var materialArr = [material_2, material_1, material_1, material_1, material_1, material_1];
// 设置数组材质对象作为网格模型材质参数
var mesh = new THREE.Mesh(geometry, materialArr);
scene.add(mesh);
三角面的材质索引属性.materialIndex
三角形面Face3可以设置材质索引属性.materialIndex
,指向数组材质中的材质对象,表达的意思是数组材质中哪一个元素用于渲染该三角形面Face3
。
一个几何体对象的不同三角面Face3可以通过材质索引设置不同的材质。
自定义Face3的材质索引
var geometry = new THREE.PlaneGeometry(204, 102, 4, 4); //矩形平面
// 材质对象1
var material1 = new THREE.MeshPhongMaterial({
color: 0xffff3f,
})
// 材质对象2
var material2 = new THREE.MeshPhongMaterial({
color: 0x0000ff,
});
// 数组材质
var materialArr = [material1, material2];
// 设置几何体的材质索引(对于PlaneGeometry而言所有Face3的材质索引默认0)
geometry.faces[4].materialIndex = 1;
geometry.faces[5].materialIndex = 1;
var mesh = new THREE.Mesh(geometry, materialArr);
scene.add(mesh);
BufferGeometry的材质索引
.groups
:Arraystart
第几个顶点的下标count
顶点数量materialIndex
材质索引
var bufferGeometry = new THREE.BufferGeometry();
// Geometry转化为BufferGeometry
bufferGeometry.fromGeometry(geometry);
// 查看材质索引materialIndex
console.log(bufferGeometry);
4. 纹理对象Texture(阵列、偏移、旋转)
阵列 repeat
-
阵列方向
texture.wrapS
水平方向texture.wrapT
垂直方向
-
阵列模式
ClampToEdgeWrapping
默认值,不重复
RepeatWrapping
阵列
MirroredRepeatWrapping
镜像阵列
ar geometry = new THREE.PlaneGeometry(200, 100); //矩形平面
var textureLoader = new THREE.TextureLoader();Texture
var texture = textureLoader.load('太阳能板2.png');
// 设置阵列模式 默认ClampToEdgeWrapping RepeatWrapping:阵列(重复) 镜像阵列:MirroredRepeatWrapping
texture.wrapS = THREE.RepeatWrapping; // 水平方向
texture.wrapT = THREE.RepeatWrapping; // 垂直方向
// uv两个方向纹理重复数量
texture.repeat.set(4, 2); // Vector2
// texture.repeat = new THREE.Vector2(4, 2)
var material = new THREE.MeshLambertMaterial({
map: texture,
});
偏移 offset
偏移范围-1~1。
纹理贴图效果:
-
不设置阵列,仅设置偏移:会有空白纹理覆盖不到的区域
var geometry = new THREE.PlaneGeometry(200, 100); var textureLoader = new THREE.TextureLoader(); var texture = textureLoader.load('太阳能板2.png'); // 不设置重复 偏移范围-1~1 texture.offset = new THREE.Vector2(0.3, 0.1) var material = new THREE.MeshLambertMaterial({ map: texture, });
-
设置阵列效果的同时,设置偏移:纹理全覆盖
var geometry = new THREE.PlaneGeometry(200, 100); var textureLoader = new THREE.TextureLoader(); var texture = textureLoader.load('太阳能板2.png'); // 设置阵列模式 texture.wrapS = THREE.MirroredRepeatWrapping; texture.wrapT = THREE.MirroredRepeatWrapping; // uv两个方向纹理重复数量 texture.repeat.set(4, 2); // 偏移效果 texture.offset = new THREE.Vector2(0.5, 0.5)
旋转 rotation
var geometry = new THREE.PlaneGeometry(200, 100);
var textureLoader = new THREE.TextureLoader();
var texture = textureLoader.load('太阳能板2.png');
// 设置纹理旋转角度
texture.rotation = Math.PI/4;
// 设置纹理的旋转中心,默认(0,0)
texture.center.set(0.5,0.5);
var material = new THREE.MeshLambertMaterial({
map: texture,
});
默认旋转中心(0, 0):
设置旋转中心(0.5,0.5):
5. canvas画布、视频作为纹理贴图
- canvas
var geometry = new THREE.PlaneGeometry(128, 32); //矩形平面
/**
* 创建一个canvas对象,并绘制一些轮廓
*/
var canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 128;
var c = canvas.getContext('2d');
// 矩形区域填充背景
c.fillStyle = "#ff00ff";
c.fillRect(0, 0, 512, 128);
c.beginPath();
// 文字
c.translate(256, 64);
c.fillStyle = "#000000"; //文本填充颜色
c.font = "bold 48px 宋体"; //字体样式设置
c.textBaseline = "middle"; //文本与fillText定义的纵坐标
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
c.fillText("three.js入门", 0, 0);
// canvas画布对象作为CanvasTexture的参数重建一个纹理对象
var texture = new THREE.CanvasTexture(canvas);
// 等同于
// var texture = new THREE.Texture(canvas);
// texture.needsUpdate = true;
var material = new THREE.MeshPhongMaterial({
map: texture, // 设置纹理贴图
});
- 视频
var geometry = new THREE.PlaneGeometry(108, 71); //矩形平面
// 创建video对象
let video = document.createElement('video');
video.src = "1086x716.mp4"; // 设置视频地址
video.autoplay = true; //要设置播放
// video对象作为VideoTexture参数创建纹理对象
var texture = new THREE.VideoTexture(video)
var material = new THREE.MeshPhongMaterial({
map: texture, // 设置纹理贴图
});
6. 法线贴图.normalMap
一个复杂的曲面模型,往往模型顶点数量比较多,模型文件比较大,为了降低模型文件大小,法线贴图.normalMap
算法自然就产生了,复杂的三维模型3D美术可以通过减面操作把精模简化为简模,然后把精模表面的复杂几何信息映射到法线贴图.normalMap
上。低模+法线贴图=高模,降低模型大小,减少顶点计算,节约顶点数量。
法线贴图通过RGB三个分量分别表示法向量的xyz三个方向。通过图片保留几何体表面的几何细节
把一个携带圆形凹坑信息的法线贴图3_256.jpg
设置到立方体网格模型的面上,你可以看到面上多个凹陷效果。
var geometry = new THREE.BoxGeometry(100, 100, 100);
var textureLoader = new THREE.TextureLoader();
// 加载法线贴图
var textureNormal = textureLoader.load('./法线贴图/3_256.jpg');
var material = new THREE.MeshPhongMaterial({
color: 0xff0000,
normalMap: textureNormal, //法线贴图
//设置深浅程度,默认值(1,1)。
normalScale: new THREE.Vector2(3, 3),
});
法线贴图:
凹陷效果:
7. 凹凸贴图.bumpMap
凹凸贴图和法线贴图功能相似,只是没有法线贴图表达的几何体表面信息更丰富。凹凸贴图是用图片像素的灰度值表示几何表面的高低深度,如果模型定义了法线贴图,就没有必要在使用凹凸贴图。
var geometry = new THREE.PlaneGeometry(400, 400);
var textureLoader = new THREE.TextureLoader();
// 加载纹理贴图
var texture = textureLoader.load('./凹凸贴图/diffuse.jpg');
// 加载凹凸贴图
var textureBump = textureLoader.load('./凹凸贴图/bump.jpg');
var material = new THREE.MeshPhongMaterial({
map: texture,// 普通纹理贴图
bumpMap:textureBump, // 凹凸贴图
bumpScale:3, // 设置凹凸高度,默认值1。
});
使用凹凸贴图和不使用凹凸贴图的视觉效果:
8. 光照贴图添加阴影.lightMap
设置模型的阴影是通过实时计算得到的,而光照贴图·lightMap
是3D美术渲染好提供给程序员。这两种方式相比较通过贴图的方式更为节约资源,提高渲染性功能。缺点是光照贴图添加的阴影是固定的,不会随着物体旋转而改变。
材质属性
- lightmap 光照贴图
- lightMapIntensity 光照强度,默认值1
一般通过Threejs几何体API创建的几何体默认只有一组纹理坐标
Geometry.faceVertexUvs[0]
,所以为了设置光照阴影贴图,需要给另一组纹理坐标赋值Geometry.faceVertexUvs[1] = Geometry.faceVertexUvs[0]
;
var geometry = new THREE.BoxGeometry(40, 100, 40); //创建一个立方体几何对象Geometry
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
//创建一个平面几何体作为投影面
var planeGeometry = new THREE.PlaneGeometry(300, 200);
// 为了设置光照阴影贴图,需要给另一组纹理坐标赋值
planeGeometry.faceVertexUvs[1] = planeGeometry.faceVertexUvs[0];
var textureLoader = new THREE.TextureLoader();
var textureLight = textureLoader.load('shadow.png');
var planeMaterial = new THREE.MeshLambertMaterial({
color: 0x999999,
lightMap:textureLight,// 设置光照贴图
// lightMapIntensity:0.5,// 光照的强度. 默认 1.
});
var planeMesh = new THREE.Mesh(planeGeometry, planeMaterial); //网格模型对象Mesh
scene.add(planeMesh); //网格模型添加到场景中
planeMesh.rotateX(-Math.PI / 2); //旋转网格模型
planeMesh.position.y = -50; //设置网格模型y坐标
阴影是固定的,不会随着光照改变:
光照阴影实时计算
var geometry = new THREE.BoxGeometry(40, 100, 40);
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 设置产生投影的网格模型
mesh.castShadow = true;
//创建一个平面几何体作为投影面
var planeGeometry = new THREE.PlaneGeometry(300, 200);
var planeMaterial = new THREE.MeshLambertMaterial({
color: 0x999999
});
var planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);
planeMesh.rotateX(-Math.PI / 2);
planeMesh.position.y = -50;
// 设置接收阴影的投影面
planeMesh.receiveShadow = true;
// 环境光
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);
// 聚光光源
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(50, 90, 50);
spotLight.angle = Math.PI /6
scene.add(spotLight);
// 3. 设置用于计算阴影的光源对象
spotLight.castShadow = true;
// 4. 设置计算阴影的区域,注意包裹对象的周围
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 300;
spotLight.shadow.camera.fov = 20;
...
9. 高光贴图 .specularMap
高光网格材质MeshPhongMaterial具有高光属性.specular
,如果一个网格模型Mesh
都是相同的材质并且表面粗糙度相同,或者说网格模型外表面所有不同区域的镜面反射能力相同,可以直接设置材质的高光属性.specular
。如果一个网格模型表示一个人,那么人的不同部位高光程度是不同的,不可能直接通过.specular
属性来描述,在这种情况通过高光贴图.specularMap
的RGB值来描述不同区域镜面反射的能力,.specularMap
和颜色贴图.Map
一样和通过UV坐标映射到模型表面。高光贴图.specularMap
不同区域像素值不同,表示网格模型不同区域的高光值不同。
下面是一个地球的案例,地球地面和海面的高光值是不同的,海面更为高亮:
var geometry = new THREE.SphereGeometry(100, 35, 35); //球体
var textureLoader = new THREE.TextureLoader();
// 加载纹理贴图
var texture = textureLoader.load('earth_diffuse.png');
// 加载高光贴图
var textureSpecular = textureLoader.load('earth_specular.png');
var material = new THREE.MeshPhongMaterial({
// specular: 0xff0000,//高光部分的颜色
shininess: 30,//高光部分的亮度,默认30
map: texture,// 普通纹理贴图
specularMap: textureSpecular, //高光贴图
});
高光贴图:
高光效果:
10. 环境贴图.envMap
环境贴图.envMap
字面意思就是三维模型周边环境,比如你渲染一个立方体,立方体放在一个屋子里面,屋子里面的周边环境肯定影响立方体的渲染效果,目的是为了渲染该立方体而不是立方体周围环境,为了更方便所以没必要创建立方体周边环境所有物体的网格模型,可以通过图片来表达立方体周边的环境。
var geometry = new THREE.BoxGeometry(100, 100, 100); //立方体
var loader = new THREE.CubeTextureLoader();
// 所有贴图在同一目录下,可以使用该方法设置共用路径
loader.setPath('环境贴图/');
// 立方体纹理加载器返回立方体纹理对象CubeTexture
var CubeTexture = loader.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
//材质对象Material
var material = new THREE.MeshPhongMaterial({
envMap: CubeTexture, //设置环境贴图
// 环境贴图反射率
// reflectivity: 0.1,
});
贴图总结
材质对象:
- 颜色贴图:模型会从纹理贴图上采集像素值
.map
: Texture
- 法线贴图:通过图片保留几何体表面的几何细节,降低模型大小,减少顶点计算,节约顶点数量
.normalMap
: Texture.normalScale
: Vector2 深浅程度,默认值(1,1)
通过RGB三个分量分别表示法向量的xyz三个方向
- 凹凸贴图:用图片像素的灰度值表示几何表面的高低深度
.bumpMap
: Texture.bumpScale
: Float 深浅程度,默认值1
- 光照(阴影)贴图:添加的阴影是固定的,不会随着物体旋转而改变
.lightmap
: Texture.lightMapIntensity
: Float 光照程度,默认值1
一般通过Threejs几何体API创建的几何体默认只有一组纹理坐标
Geometry.faceVertexUvs[0]
,需要给投影面的另一组纹理坐标赋值`Geometry.faceVertexUvs[1] = Geometry.faceVertexUvs[0] - 高光贴图:通过高光贴图的RGB值来描述不同区域镜面反射的能力
.specularMap
: Texture.shininess
: Float 高光部分的亮度,默认值30
如果一个网格模型
Mesh
都是相同的材质并且表面粗糙度相同,或者说网格模型外表面所有不同区域的镜面反射能力相同,可以直接设置材质的高光属性.specular
(高光颜色)。 - 环境贴图:通过图片来表达立方体周边的环境
.envMap
: Texture.reflectivity
: Float 反射率,默认值1
MeshLambertMaterial
、MeshBasicMaterial
没有凹凸、法线、高光贴图属性。
11. 数组纹理对象DataTexture
通过程序创建纹理贴图的每一个像素值。
- RGB:
THREE.RGBFormat
var geometry = new THREE.PlaneGeometry(128, 128); //矩形平面
/**
* 创建纹理对象的像素数据
*/
var width = 32; //纹理宽度
var height = 32; //纹理高度
var size = width * height; //像素大小
var data = new Uint8Array(size * 3); //size*3:像素在缓冲区占用空间
for (let i = 0; i < size * 3; i += 3) {
// 随机设置RGB分量的值
data[i] = 255 * Math.random()
data[i + 1] = 255 * Math.random()
data[i + 2] = 255 * Math.random()
}
// 创建数据文理对象 RGB格式:THREE.RGBFormat
var texture = new THREE.DataTexture(data, width, height, THREE.RGBFormat);
texture.needsUpdate = true; //纹理更新
- RGBA:
THREE.RGBAFormat
...
var data = new Uint8Array(size * 4); //size*4:像素在缓冲区占用空间
for (let i = 0; i < size * 4; i += 4) {
// 随机设置RGB分量的值
data[i] = 255 * Math.random()
data[i + 1] = 255 * Math.random()
data[i + 2] = 255 * Math.random()
// 设置透明度分量A
data[i + 3] = 255 * 0.5
}
// 创建数据文理对象 RGBA格式:THREE.RGBAFormat
var texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat);
texture.needsUpdate = true; //纹理更新
九、相机对象(投影方式)
基类Object3D
1. 正交投影相机和透视投影相机
正投影和透视投影简单解释
三维几何体在人眼睛中的效果就像一张相机拍摄的二维照片,你看到的是一个2D的投影图。 空间几何体转化为一个二维图的过程就是投影,不同的投影方式意味着投影尺寸不同的算法。相机拍照,本质就是投影计算的过程。
- 正投影
- 小的场景,比如产品在线展示、机械、工业设计类三维建模软件模型的显示
- 物体角度不同投影长度不同,不受物体与相机的距离影响
- 透视投影
- 大的场景,比如游戏场景
- 投影结果受物体角度与距离相机的距离影响
正交投影相机OrthographicCamera
OrthographicCamera( left, right, top, bottom, near, far )
left
渲染空间的左边界right
渲染空间的右边界top
渲染空间的上边界bottom
渲染空间的下边界near
近裁(从距离相机多远的位置开始渲染),一般情况会设置一个很小的值。 默认值0.1far
远裁(距离相机多远的位置截止渲染),如果设置的值偏小小,会有部分场景看不到。 默认值1000
左右边界的距离与上下边界的距离比值与画布的渲染窗口的宽高比例要一致,否则三维模型的显示效果会被单方向不等比例拉伸。
var geometry = new THREE.BoxGeometry(100, 100, 100);
...
/**
* 正投影相机设置
*/
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); //指向场景对象
透视投影相机PerspectiveCamera
PerspectiveCamera( fov, aspect, near, far )
fov
视场(能够看到的角度范围),默认值45aspect
渲染窗口的长宽比,默认值window.innerWidth/window.innerHeight
near
近裁(从距离相机多远的位置开始渲染),一般情况会设置一个很小的值。 默认值0.1far
远裁(距离相机多远的位置截止渲染),如果设置的值偏小小,会有部分场景看不到。 默认值1000
人的眼睛大约能够看到180度的视场,视角大小设置要根据具体应用,一般游戏会设置60~90度。
var geometry = new THREE.BoxGeometry(100, 100, 100);
...
/**
* 透视投影相机设置
*/
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); //指向场景对象
相机位置.posiiotn
与方向.lookAt()
通过相机观察点的位置.posiiotn
和.lookAt()
方法指向的位置就可以计算出相机的拍摄角度,本质上就是计算出相机对象的视图矩阵.matrixWorldInverse
(世界矩阵matrixWorld的逆矩阵)。
对于透视投影而言,相机位置与lookAt
指向的观察目标位置间隔距离越小,场景中的三维模型放大倍数越大,准确地说是透视投影相机可以拍摄的范围更小,同时场景Scene
中超出的相机参数约束范围的部分会被剪裁掉。
视图矩阵.matrixWorldInverse
和投影矩阵.projectionMatrix
对相机对象一些参数的设置本质上就是设置相机对象的视图矩阵.matrixWorldInverse
和投影矩阵.projectionMatrix
属性,Threejs渲染的时候会通过你设置的相机参数按照一定的算法计算出来投影和视图矩阵值,根据投影和视图矩阵值对顶点进行矩阵变换
- 影响视图矩阵
.matrixWorldInverse
计算的相关代码
camera.position.set(200, 300, 200); //设置相机位置 camera.lookAt(scene.position); //设置相机方向
-
影响投影矩阵
.projectionMatrix
计算相机参数的相关代码- 正投影
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000); // 正投影相机对象
- 透视投影
var camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000); // 透视投影相机对象
2. 窗口变换自适应渲染
开发的过程中你可能会遇到这样一个问题,通过鼠标拖动使浏览器的窗口变大,因为Threejs渲染器的渲染尺寸范围没有跟着变化,出现局部空白区域。对于这种情况要做的就是重新获取浏览器窗口新的宽高尺寸,然后通过新的宽高尺寸更新相机Camera和渲染器WebGLRenderer的参数即可。
- 获取浏览器窗口新的宽高尺寸,重置渲染器输出画布canvas的尺寸
renderer.setSize(window.innerWidth,window.innerHeight);
-
通过新的宽高尺寸重置相机Camera
- 正投影相机
// 设置相机上下左右四个边界属性 k = window.innerWidth/window.innerHeight;//窗口宽高比 camera.left = -s*k; camera.right = s*k; camera.top = s; camera.bottom = -s;
- 透视投影相机
// 设置渲染窗口的长宽比 camera.aspect = window.innerWidth/window.innerHeight;
-
更新相机的投影矩阵
camera.updateProjectionMatrix();
正投影上下左右边界属性、透视投影长宽比属性变化,都会影响到各自投影矩阵的变化。
渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix,但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)。
如果相机的一些属性发生了变化,需要执行
updateProjectionMatrix()
方法更新相机的投影矩阵。
- 正投影相机OrthographicCamera自适应渲染
window.onresize=function(){
// 重置渲染器输出画布canvas尺寸
renderer.setSize(window.innerWidth,window.innerHeight);
// 重置相机投影的相关参数(宽高相关)
k = window.innerWidth/window.innerHeight;//窗口宽高比
camera.left = -s*k;
camera.right = s*k;
camera.top = s;
camera.bottom = -s;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix ();
};
- 透视投影相机PerspectiveCamera自适应渲染
window.onresize=function(){
// 重置渲染器输出画布canvas尺寸
renderer.setSize(window.innerWidth,window.innerHeight);
// 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
camera.aspect = window.innerWidth/window.innerHeight;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix ();
};
十、精灵模型、粒子系统
1. 精灵模型对象Sprite
创建精灵模型对象Sprite
和创建网格模型对象一样需要创建一个材质对象,不同的地方在于创建精灵模型对象不需要创建几何体对象Geometry
,精灵模型对象本质上你可以理解为已经内部封装了一个平面矩形几何体PlaneGeometry
,矩形精灵模型与矩形网格模型的区别在于精灵模型的正面永远朝着屏幕。
通过Sprite
创建精灵模型不需要几何体,只需要给构造函数Sprite
的参数设置为一个精灵材质SpriteMaterial即可。
精灵材质对象SpriteMaterial
(基类Material
)和普通的网格材质一样可以设置颜色.color
、颜色贴图.map
、开启透明.transparent
、透明度.opacity
等属性。
// 创建精灵材质对象SpriteMaterial
var spriteMaterial = new THREE.SpriteMaterial({
color:0xff00ff,//设置精灵矩形区域颜色
rotation: Math.PI/4,//绕垂直屏幕方向旋转45度,弧度值
lights: false, // 受光照影响
// map: texture,//设置精灵纹理贴图
});
// 创建精灵模型对象,不需要几何体geometry参数
var sprite = new THREE.Sprite(spriteMaterial);
scene.add(sprite);
// 控制精灵大小,比如可视化中精灵大小表征数据大小
sprite.scale.set(10, 10, 1); // 只需要设置x、y两个分量就可以
sprite.position.set(10, 10, 1); // 设置精灵位置
2. 中国城市PM2.5可视化案例
// 加载一个背景透明的圆形贴图,矩形精灵显示为圆形效果
var texture = new THREE.TextureLoader().load("sprite.png");
// 创建组对象,包含所有精灵对象
let group = new THREE.Group();
// 文件加载对象
var loader = new THREE.FileLoader().setResponseType('json');
// 加载PM2.5数据
loader.load('数据.json', function(data) {
//遍历数据
data.forEach(elem => {
// 精灵材质
var spriteMaterial = new THREE.SpriteMaterial({
map: texture, //设置精灵纹理贴图
transparent: true,
opacity: 0.5,
});
// 创建精灵模型对象
var sprite = new THREE.Sprite(spriteMaterial);
group.add(sprite);
// 控制精灵大小 使用PM2.5大小设置精灵模型的大小
// 纹理贴图较大,注意适当缩放精灵模型大小,以便得到更好的显示效果
var k = elem.value / 200
sprite.scale.set(k, k, 1);
// 获得城市坐标设置精灵模型对象的位置
sprite.position.set(elem.coordinate[0], elem.coordinate[1], 0)
});
// 中国城市坐标整体的几何中心不在坐标原点,需要适当的平移
group.position.set(-110, -30, 0);
scene.add(group);//把精灵群组插入场景中
})
// 精灵模型正面永远朝向平面,旋转可能导致精灵模型都堆叠在一起
// 禁止旋转操作
controls.enableRotate = false;
鼠标旋转导致精灵模型都堆叠在一起:
如果你想实现更好的效果,也可以使用矩形网格模型Mesh代替精灵模型,主要是场景旋转的时候,精灵模型平面总是平行于canvas画布。
// 加载一个背景透明的圆形贴图,矩形精灵显示为圆形效果
var texture = new THREE.TextureLoader().load("sprite.png");
// 创建组对象,包含所有精灵对象
let group = new THREE.Group();
// 文件加载对象
var loader = new THREE.FileLoader().setResponseType('json');
// 加载PM2.5数据
loader.load('数据.json', function(data) {
//遍历数据
data.forEach(elem => {
var material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.5,
})
// 矩形平面几何体
var geometry = new THREE.PlaneGeometry(1, 1)
var mesh = new THREE.Mesh(geometry, material)
group.add(mesh);
// 控制精灵大小 使用PM2.5大小设置精灵模型的大小
// 注意适当缩放pm2.5大小,以便得到更好的显示效果
var k = elem.value / 200
mesh.scale.set(k, k, 1);
//获得城市坐标设置精灵模型对象的位置
mesh.position.set(elem.coordinate[0], elem.coordinate[1], 0)
});
// 中国城市坐标整体的几何中心不在坐标原点,需要适当的平移
group.position.set(-110, -30, 0);
scene.add(group); //把精灵群组插入场景中
})
可以正常旋转,反面网格模型不可见:
3. 下雨场景效果模拟
基本思路就是通过足够多的精灵模型构成一个粒子系统,然后每一个雨滴按照在一定空间内随机分布,每个精灵模型都使用一个背景透明的雨滴rain.png
作为纹理贴图。
var scene = new THREE.Scene();
/**
* 创建一个地面
*/
var geometry = new THREE.PlaneGeometry(1000, 1000); //矩形平面
// 加载草地纹理贴图
var texture = new THREE.TextureLoader().load("grass.jpg");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
texture.repeat.set(10, 10);
var material = new THREE.MeshLambertMaterial({
map:texture,
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
mesh.rotateX(-Math.PI/2) // 放平草地
/**
* 精灵创建下雨效果
*/
// 加载雨滴纹理贴图
var textureTree = new THREE.TextureLoader().load("rain.png");
// 创建一个组表示所有的雨滴
var group = new THREE.Group();
// > 创建大量的雨精灵分布在空间中
// 批量创建雨滴精灵模型
for (let i = 0; i < 2000; i++) {
var spriteMaterial = new THREE.SpriteMaterial({
map: textureTree, //设置精灵纹理贴图
});
// 创建精灵模型对象
var sprite = new THREE.Sprite(spriteMaterial);
group.add(sprite);
// 控制精灵大小,
sprite.scale.set(8, 10, 1); //// 只需要设置x、y两个分量就可以
var k1 = Math.random() - 0.5;
var k2 = Math.random() - 0.5;
// > 放大雨精灵分布的空间范围
// 设置精灵模型位置,在空间中随机分布
sprite.position.set(1000 * k1, 300 * Math.random(), 1000 * k2)
}
scene.add(group); //雨滴群组插入场景中
//点光源
var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
//环境光
var ambient = new THREE.AmbientLight(0x888888);
scene.add(ambient);
/**
* 透视投影相机设置
* 为了更好观察下雨的效果,就像人站在实际的雨中一样,需要使用透视投影相机PerspectiveCamera
* 同时把透视投影相机对象的位置设置在雨粒子系统里面而不是雨粒子系统的外面。
*/
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
//透视投影相机对象
var camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
// > 把透视投影相机对象的位置设置在雨粒子系统里面
camera.position.set(292, 109, 268);//设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
// 渲染器
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); //设置渲染区域尺寸
// renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
/*
* 匀速下落
*/
var clock = new THREE.Clock()
// 渲染函数
function render() {
// 两次渲染时间间隔
var t = clock.getDelta();
// 每次渲染遍历雨滴群组,刷新频率30~60FPS,两帧时间间隔16.67ms~33.33ms
// 每次渲染都会更新雨滴的位置,进而产生动画效果
group.children.forEach(sprite => {
// 雨滴的y坐标每次减1
// sprite.position.y -= 1;
sprite.position.y -= 50 * t;
if (sprite.position.y < 0) {
// 如果雨滴落到地面,重置y,从新下落
sprite.position.y = 200;
}
});
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}
render();
// 创建控件对象
var controls = new THREE.OrbitControls(camera,renderer.domElement);
十一、帧动画
Threejs提供了一系列用户编辑和播放关键帧动画的API:
- 编辑
- 关键帧
KeyframeTrack
- 位置、颜色等属性随着时间变化
- 离散时间点对应离散属性值
- 剪辑
AnimationClip
- 多个关键帧构成一个剪辑clip对象
- 关键帧
- 播放
- 混合器
AnimationMixer
- 一个对象及其子对象的动画播放器
- 操作
AnimationAction
- 设置播放方式、开始/暂停播放。。。
- 混合器
一般不会手动编辑,都是外部模型编辑好了动画直接播放。
1. 编辑关键帧并解析播放
/**
* 创建两个网格模型并设置一个父对象group
*/
var group = new THREE.Group(); //作为网格模型的父对象
// 网格模型1
var geometry1 = new THREE.BoxGeometry(40, 6, 6); //长方体
var material1 = new THREE.MeshLambertMaterial({
color: 0x0000ff
});
var mesh1 = new THREE.Mesh(geometry1, material1);
mesh1.name = "Box"; //网格模型命名
group.add(mesh1); //网格模型添加到组中
// 网格模型2
var geometry2 = new THREE.SphereGeometry(10, 25, 25); //球体
var material2 = new THREE.MeshLambertMaterial({
color: 0xff00ff
});
var mesh2 = new THREE.Mesh(geometry2, material2);
mesh2.name = "Sphere"; //网格模型命名
group.add(mesh2); //网格模型添加到组中
scene.add(group); //组添加到场景中中
/**
* 编辑group子对象网格模型mesh1和mesh2的帧动画数据
*/
// 创建名为Box对象的关键帧数据
var times = [0, 10]; //关键帧时间数组,离散的时间点序列
var values = [0, 0, 0, 150, 0, 0]; // 与时间点对应的值组成的数组
// 创建位置关键帧对象:0时刻对应位置0, 0, 0 10时刻对应位置150, 0, 0
var posTrack = new THREE.KeyframeTrack('Box.position', times, values);
// 创建颜色关键帧对象:10时刻对应颜色1, 0, 0 20时刻对应颜色0, 0, 1
var colorKF = new THREE.KeyframeTrack('Box.material.color', [10, 20], [1, 0, 0, 0, 0, 1]);
// 创建名为Sphere对象的关键帧数据 从0~20时间段,尺寸scale缩放3倍
var scaleTrack = new THREE.KeyframeTrack('Sphere.scale', [0, 20], [1, 1, 1, 3, 3, 3]);
// 剪辑
// duration决定了默认的播放时间,一般取所有帧动画的最大时间
// duration偏小,帧动画数据无法播放完,偏大,播放完帧动画会继续空播放
var duration = 20; // 播放结束时间,而不是播放时长
// 多个帧动画作为元素创建一个剪辑clip对象,命名"default",持续时间20
var clip = new THREE.AnimationClip("default", duration, [posTrack, colorKF, scaleTrack]);
/**
* 播放编辑好的关键帧数据
*/
// group作为混合器的参数,可以播放group中所有子对象的帧动画
var mixer = new THREE.AnimationMixer(group);
// 剪辑clip作为参数,通过混合器clipAction方法返回一个操作对象AnimationAction
var AnimationAction = mixer.clipAction(clip);
//通过操作Action设置播放方式
AnimationAction.timeScale = 20;//默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
AnimationAction.play();//开始播放
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
render();
2. 解析外部模型的帧动画
var loader = new THREE.ObjectLoader();
var mixer = null; //声明一个混合器变量
// 加载文件返回一个对象obj
loader.load("model.json", function(obj) {
// console.log(obj)
obj.scale.set(15, 15, 15);
scene.add(obj);
// obj作为混合器的参数,可以播放obj包含的帧动画数据
mixer = new THREE.AnimationMixer(obj);
// obj.animations[0]:获得剪辑clip对象
// 剪辑clip作为参数,通过混合器clipAction方法返回一个操作对象AnimationAction
var AnimationAction = mixer.clipAction(obj.animations[0]);
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play();
});
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
// 模型文件的加载是异步的,需要判断混合器是否存在再进行更新
if(mixer!==null){
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
}
render();
3. 播放设置
- 播放/暂停
.paused
// 暂停继续播放函数
function pause() {
if (AnimationAction.paused) {
// 如果是播放状态,设置为暂停状态
AnimationAction.paused = false;
} else {
// 如果是暂停状态,设置为播放状态
AnimationAction.paused = true;
}
}
- 播放clip特定时间段
// 设置播放区间10~18
AnimationAction.time = 10; //操作对象设置开始播放时间
clip.duration = 18; // 剪辑对象设置播放结束时间
AnimationAction.play(); //开始播放
- 定位在某个时间点
// 开始结束时间设置为一样,相当于播放时间为0,直接跳转到时间点对应的状态
AnimationAction.time = 10; //操作对象设置开始播放时间
clip.duration = AnimationAction.time;//剪辑对象设置播放结束时间
AnimationAction.play(); //开始播放
十二、骨骼动画、变形动画
1. 骨骼动画原理
直接使用Threejs编写一个骨骼动画还是比较复杂的,你首先应该了解骨头关节Bone、骨骼网格模型SkinnedMesh、骨架对象Skeleton这三个骨骼相关的类,除此之外还需要了解几何体Geometry和骨骼动画相关的顶点数据。
-
骨关节Bone
- 树结构:一个父关节可以有多个子关节
- 设置子关节的位置是相对父关节的位置
var Bone1 = new THREE.Bone(); //关节1 var Bone2 = new THREE.Bone(); //关节2 // 通过add方法给一个骨关节对象添加一个子骨关节 // 多个骨头关节构成一个树结构 Bone1.add(Bone2); // 设置关节父子关系:关节1作为根关节 // Bone2是Bone1的子关节,通过Bone1的`children`属性可以访问Bone2 console.log(Bone1.children) // [Bone] // 设置关节之间的**相对位置** // 根关节Bone1默认位置是(0,0,0) Bone2.position.y = 60; // Bone2相对父对象Bone1位置
-
骨架对象Skeleton
THREE.Skeleton([Bone1, Bone2, Bone3])
.bones
: Array 包含所有骨头关节对象
// 所有Bone对象插入到Skeleton中 var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统 // 查看骨架相关的所有骨关节 console.log(skeleton.bones); // 返回所有关节的世界坐标 skeleton.bones.forEach(elem => { console.log(elem.getWorldPosition(new THREE.Vector3())); });
-
几何体Geometry
.skinWeights
: Array[Vector4] 顶点蒙皮权重- 每个顶点最多可以有4个骨骼对象Bone影响它
- 顶点可以被4个骨骼Bone修改,因此Vector4用于表示该顶点的蒙皮权重
- 向量vector分量的值通常应在0和1之间
.skinIndices
: Array[Vector4] 顶点蒙皮索引- 索引对应骨架对象的.bones属性的元素索引
几何体
Geometry
的属性.skinWeights
和.skinIndices
主要作用是用来设置几何体的顶点位置是如何受骨关节运动影响的。 -
骨骼网格模型SkinnedMesh
- 骨头Bone构成骨架Skeleton
SkinnedMesh.add(Bone1);
添加关节。通过SkinnedMesh的.children
属性可以访问骨关节Bone
- 网格模型绑定骨架
SkinnedMesh.bind(skeleton)
绑定骨架。通过SkinnedMesh的skeleton
属性可以访问骨架Skeleton
SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型 SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统
- 骨头Bone构成骨架Skeleton
手动创建一个骨骼网格模型
/**
* 创建骨骼网格模型SkinnedMesh
*/
// 创建一个圆柱几何体,高度120,顶点坐标y分量范围[-60,60]
var geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
geometry.translate(0, 60, 0); //平移后,y分量范围[0,120]
/**
* 设置几何体对象Geometry的蒙皮索引skinIndices、权重skinWeights属性
* 实现一个模拟腿部骨骼运动的效果
*/
//遍历几何体顶点,为每一个顶点设置蒙皮索引、权重属性
//根据y来分段,0~60一段、60~100一段、100~120一段
for (var i = 0; i < geometry.vertices.length; i++) {
var vertex = geometry.vertices[i]; //第i个顶点
if (vertex.y <= 60) {
// 设置每个顶点蒙皮索引属性 受根关节Bone1影响
geometry.skinIndices.push(new THREE.Vector4(0, 0, 0, 0));
// 设置每个顶点蒙皮权重属性
// 影响该顶点关节Bone1对应权重是1-vertex.y/60
geometry.skinWeights.push(new THREE.Vector4(1 - vertex.y / 60, 0, 0, 0));
} else if (60 < vertex.y && vertex.y <= 60 + 40) {
// Vector4(1, 0, 0, 0)表示对应顶点受关节Bone2影响
geometry.skinIndices.push(new THREE.Vector4(1, 0, 0, 0));
// 影响该顶点关节Bone2对应权重是1-(vertex.y-60)/40
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 60) / 40, 0, 0, 0));
} else if (60 + 40 < vertex.y && vertex.y <= 60 + 40 + 20) {
// Vector4(2, 0, 0, 0)表示对应顶点受关节Bone3影响
geometry.skinIndices.push(new THREE.Vector4(2, 0, 0, 0));
// 影响该顶点关节Bone3对应权重是1-(vertex.y-100)/20
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 100) / 20, 0, 0, 0));
}
}
// 材质对象
var material = new THREE.MeshPhongMaterial({
skinning: true, //允许蒙皮动画
});
// 创建骨骼网格模型
var SkinnedMesh = new THREE.SkinnedMesh(geometry, material);
SkinnedMesh.position.set(50, 120, 50); //设置网格模型位置
SkinnedMesh.rotateX(Math.PI); //旋转网格模型
scene.add(SkinnedMesh); //网格模型添加到场景中
/**
* 骨骼系统
*/
var Bone1 = new THREE.Bone(); //关节1,用来作为根关节
var Bone2 = new THREE.Bone(); //关节2
var Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系 多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
// 设置关节之间的相对位置
//根关节Bone1默认位置是(0,0,0)
Bone2.position.y = 60; //Bone2相对父对象Bone1位置
Bone3.position.y = 40; //Bone3相对父对象Bone2位置
console.log(Bone1.children)
// 所有Bone对象插入到Skeleton中,全部设置为.bones属性的元素
var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
console.log(skeleton.bones);
// 返回所有关节的世界坐标
// skeleton.bones.forEach(elem => {
// console.log(elem.getWorldPosition(new THREE.Vector3()));
// });
//骨骼关联网格模型
SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统
console.log(SkinnedMesh);
// 区分显示一下各骨关节
skeleton.bones[1].rotation.x = 0.5;
skeleton.bones[2].rotation.x = 0.5;
// 骨骼辅助显示
var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
scene.add(skeletonHelper);
2. 加载外部模型骨骼动画
/**
* 加载解析骨骼模型动画
*/
var loader = new THREE.ObjectLoader(); //创建一个加载器
var mixer = null; //声明一个混合器变量
loader.load("./marine_anims_core.json", function(obj) {
console.log(obj)
scene.add(obj); //添加到场景中
/* 查看骨骼数据 */
//从返回对象获得骨骼网格模型
var SkinnedMesh = obj.children[0];
// 查看骨头关节Bone
// console.log(SkinnedMesh.skeleton.bones);
// 遍历骨骼模型中的骨关节Bone,并获得世界坐标
// SkinnedMesh.traverse(function(elem) {
// if (elem.type === 'Bone') {
// console.log(elem.getWorldPosition(new THREE.Vector3()));
// }
// });
//骨骼网格模型作为参数创建一个混合器
mixer = new THREE.AnimationMixer(SkinnedMesh);
// 查看骨骼网格模型的帧动画数据
console.log(SkinnedMesh.geometry.animations)
// 解析跑步状态对应剪辑对象clip中的关键帧数据
var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[1]);
// 解析步行状态对应剪辑对象clip中的关键帧数据
// var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[3]);
AnimationAction.play();
// 骨骼辅助显示
// var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
// scene.add(skeletonHelper);
})
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
if (mixer !== null) {
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
}
render();
3. 变形目标动画原理
关于变形动画,你可以理解为多组顶点数据,从一个状态变化到另一个状态,比如人的面部表情,哭的表情用一系列的顶点表示,笑的表情用一系列的顶点表示,从哭的表情过渡到笑的表情,就是表情对应的两组顶点之间的过渡,几何体的顶点的位置坐标发生变化,从一个状态过渡到另一个状态自然就产生了变形动画。
- 通过几何体Geometry的变形目标属性
.morphTargets
设置好变形动画
/**
* 创建网格模型,并给模型的几何体设置多个变形目标
*/
// 创建一个几何体
var geometry = new THREE.BoxGeometry(50, 50, 50); //立方体几何对象
// 为geometry提供变形目标的数据
var box1 = new THREE.BoxGeometry(100, 5, 100); //立方体(为变形目标1提供数据)
var box2 = new THREE.BoxGeometry(5, 200, 5); //立方体(为变形目标2提供数据)
// 设置变形目标的数据
geometry.morphTargets[0] = {name: 'target1',vertices: box1.vertices};
geometry.morphTargets[1] = {name: 'target2',vertices: box2.vertices};
var material = new THREE.MeshLambertMaterial({
morphTargets: true, //允许变形
color: 0x0000ff
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
- 控制变形目标影响权重
.morphTargetInfluences
(范围一般0~1)的属性值生成关键帧数据,实现关键帧动画
/**
* 设置关键帧数据
*/
//启用变形目标并设置变形目标影响权重,范围一般0~1
// mesh.morphTargetInfluences[0] = 0.5;
// mesh.morphTargetInfluences[1] = 1;
// 设置变形目标1对应权重随着时间的变化
// 创建位置关键帧对象:0时刻对应权重0 10时刻对应权重1 20时刻对应权重0
var Track1 = new THREE.KeyframeTrack('.morphTargetInfluences[0]', [0,10,20], [0,1, 0]);
// 设置变形目标2对应权重随着时间的变化
var Track2 = new THREE.KeyframeTrack('.morphTargetInfluences[1]', [20,30, 40], [0, 1,0]);
// 创建一个剪辑clip对象,命名"default",持续时间40
var clip = new THREE.AnimationClip("default", 40, [Track1,Track2]);
- 播放关键帧动画即可实现变形动画
/**
* 播放编辑好的关键帧数据
*/
var mixer = new THREE.AnimationMixer(mesh); //创建混合器
var AnimationAction = mixer.clipAction(clip); //返回动画操作对象
AnimationAction.timeScale = 5; //默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play(); //开始播放
...
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
render();
4. 解析外部模型变形目标数据
var loader = new THREE.JSONLoader(); //创建加载器
var mixer = null; //声明一个混合器变量
loader.load("./鸟/flamingo.json", function(geometry) {
// console.log(geometry);
var material = new THREE.MeshPhongMaterial({
morphTargets: true, // 材质对象开启渲染目标
vertexColors: THREE.FaceColors,
});
// 通过平均面法线来计算顶点法线,效果更光滑
geometry.computeVertexNormals();
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh); //插入到场景中
// 创建一个混合器,播放网格模型模型的变形动画
mixer = new THREE.AnimationMixer(mesh);
// geometry.animations[0]:获得剪辑对象clip
var AnimationAction=mixer.clipAction(geometry.animations[0]);
// AnimationAction.timeScale = 0.5; //默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play();//播放动画
})
变形目标生成帧动画
// var AnimationAction=mixer.clipAction(geometry.animations[0]);
// 通过该方法把多个变形目标自动化生成剪辑对象clip
// 30是fps,影响动画速度
var clip = THREE.AnimationClip.CreateFromMorphTargetSequence('default', geometry.morphTargets, 30);
var AnimationAction=mixer.clipAction(clip);
AnimationAction.timeScale = 0.5; //默认1,可以调节播放速度
十三、语言模块
本质上是对原生Web Audio API的封装。
- 监听者
AudioListener
- 音频
Audio
- 可用于不考虑位置的背景音乐
- 位置音频
PositionalAudio
- 音频源位置发生变化,听到的声音有所变化,比如音量大小
- 音频分析器
AudioAnalyser
- 音频加载器
AudioLoader
// 创建一个音频加载器
var audioLoader = new THREE.AudioLoader();
// 加载音频文件,返回一个音频缓冲区对象作为回调函数参数
audioLoader.load('./中国人.mp3', function(AudioBuffer) {
// 音频缓冲区对象关联到音频对象audio
audio.setBuffer(AudioBuffer);
audio.setLoop(true); //是否循环
audio.setVolume(0.9); //音量
// 播放缓冲区中的音频数据
audio.play(); //play播放、stop停止、pause暂停
});
1. 音频与场景关联(位置音频)
...
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
//设置相机位置,相当于设置监听者位置
camera.position.set(0, 0, 200);
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
// 用来定位音源的网格模型
var audioMesh = new THREE.Mesh(geometry, material);
// 设置网格模型的位置,相当于设置音源的位置
audioMesh.position.set(0, 0, 300);
scene.add(audioMesh);
...
render()
...
// 创建一个虚拟的监听者
var listener = new THREE.AudioListener();
// 监听者绑定到相机对象
camera.add(listener);
// 创建一个位置音频对象,监听者作为参数,音频和监听者关联。
var PosAudio = new THREE.PositionalAudio(listener);
// 音源绑定到一个网格模型上
audioMesh.add(PosAudio);
// 创建一个音频加载器
var audioLoader = new THREE.AudioLoader();
// 加载音频文件,返回一个音频缓冲区对象作为回调函数参数
audioLoader.load('./中国人.mp3', function(AudioBuffer) {
// 音频缓冲区对象关联到音频对象audio
PosAudio.setBuffer(AudioBuffer);
PosAudio.setVolume(0.9); //音量
PosAudio.setRefDistance(200); //参数值越大,声音越大
PosAudio.play(); //播放
});
2. 音乐可视化
获取频率数据,然后通过频率数据控制网格模型的长度方向伸缩变化:
/**
* 创建多个网格模型组成的组对象
*/
var group = new THREE.Group();
let N = 128; //控制音频分析器返回频率数据数量
for (let i = 0; i < N / 2; i++) {
var box = new THREE.BoxGeometry(10, 100, 10);
var material = new THREE.MeshPhongMaterial({
color: 0x0000ff
});
var mesh = new THREE.Mesh(box, material);
// 长方体间隔20,整体居中
mesh.position.set(20 * i - N / 2 * 10, 0, 0)
group.add(mesh)
}
scene.add(group)
...
var analyser = null; // 声明一个分析器变量
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
if (analyser) {
// 获得频率数据N个
var arr = analyser.getFrequencyData();
// console.log(arr);
// 遍历组对象,每个网格子对象设置一个对应的频率数据
group.children.forEach((elem, index) => {
elem.scale.y = arr[index] / 80
elem.material.color.r = arr[index] / 200;
});
}
}
render();
var listener = new THREE.AudioListener() //监听者
var audio = new THREE.Audio(listener); //非位置音频对象
var audioLoader = new THREE.AudioLoader(); //音频加载器
// 加载音频文件
audioLoader.load('中国人.mp3', function(AudioBuffer) {
audio.setBuffer(AudioBuffer); // 音频缓冲区对象关联到音频对象audio
audio.setLoop(true); //是否循环
audio.setVolume(0.5); //音量
audio.play(); //播放
// 音频分析器和音频绑定,可以实时采集音频时域数据进行快速傅里叶变换
analyser = new THREE.AudioAnalyser(audio,2*N);
});
十四、模型文件加载
1. three.js数据结构、导入导出
查看Threejs文档Geometry、Material、Light、Object3D等类,你可以发现这些类都提供了一个方法.toJSON()
通过这个方法可以导出Threejs三维模型的各类数据,该方法的功能就是把Threejs的几何体、材质、光源等对象转化为JSON格式导出。
导出数据
导出一个json文件的过程:
- 从three.js对象提取数据
.toJSON()
- 将JSON对象转为字符串
JSON.stringify()
- HTML5的文件保存模块
- 导出几何体信息
var geometry = new THREE.BoxGeometry(100, 100, 100);
console.log(geometry);
console.log(geometry.toJSON()); // JSON对象
// JSON对象转化为字符串
console.log(JSON.stringify(geometry.toJSON()));
// JSON.stringify()方法内部会自动调用参数的toJSON()方法
console.log(JSON.stringify(geometry));
- 导出材质信息
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
});
console.log(material);
console.log(material.toJSON());
console.log(JSON.stringify(material));
- 导出场景信息,包含模型、光源等等
console.log(scene);
console.log(scene.toJSON());
导入(加载解析)
加载解析过程:
-
加载json文件
-
字符串转为JSON对象
JSON.parse()
-
调用threejs相应的API创建对象
- MaterialLoader 材质
- BufferGeometryLoader 几何体
var loader = new THREE.BufferGeometryLoader(); // 缓冲几何体数据加载器 loader.load('bufferGeometry.json',function (geometry) { console.log(geometry); var material = new THREE.MeshLambertMaterial({ color: 0x0000ff, }); var mesh = new THREE.Mesh(geometry, material); scene.add(mesh); })
- AnimationLoader 帧动画
- TextureLoader 纹理
- JSONLoader
- ObjectLoader
- 网格模型对象
- 模型组
- 场景
/** * 网格模型Mesh加载,包含几何体Geometry和材质Material */ var loader = new THREE.ObjectLoader(); loader.load('model.json',function (obj) { console.log(obj); obj.scale.set(100,100,100) scene.add(obj) })
2. 加载stl文件并解析
.stl
格式的三维模型不包含材质Material信息,只包含几何体顶点数据的信息,你可以简单地把stl文件理解为几何体对象Geometry。
stl文件数据结构
solid box //文件名字
//三角面1
facet normal 0 0 -1 //三角形面法向量
outer loop
vertex 50 50 -50 //顶点位置
vertex 50 -50 -50 //顶点位置
vertex -50 50 -50 //顶点位置
endloop
endfacet
//三角面2
...
endsolid
通过STLLoader.js
加载.stl文件
// THREE.STLLoader创建一个加载器
var loader = new THREE.STLLoader();
// 立方体默认尺寸长宽高各200
loader.load('立方体.stl',function (geometry) {
// 加载完成后会返回一个几何体对象BufferGeometry
console.log(geometry);
// 查看顶点数,一个立方体6个矩形面,每个矩形面至少2个三角面,每个三角面3个顶点,
// 如果没有索引index复用顶点,就是说一个立方体至少36个顶点
console.log(geometry.attributes.position.count);
// 缩放几何体
// geometry.scale(0.5,0.5,0.5);
// 几何体居中
// geometry.center();
// 平移立方体
// geometry.translate(-50,-50,-50);
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
})
3. 加载obj文件(几何体、材质、贴图)
使用三维软件导出.obj
模型文件的时候,会同时导出一个材质文件.mtl
, .obj
和.stl
文件包含的信息一样都是几何体顶点相关数据,材质文件.mtl
包含的是模型的材质信息,比如颜色、贴图路径等。
加载.obj
三维模型的时候,可以只加载.obj
文件,然后借助three.js引擎自定义材质Material,也可以同时加载.obj
和.mtl
文件。
obj文件可以包含多个网格模型对象,不一定就是一个,这些网格模型对象全部是并列关系,无法通过父子关系构建一个树结构层级模型。
.obj
文件不包含场景的相机Camera、光源Light等信息,不能导出骨骼动画、变形动画,如果希望导出光照信息、相机信息、骨骼动画信息、变形动画信息,可以选择.fbx
、.gltf
等格式。
- 只加载obj文件:没有材质文件,系统自动设置Phong网格材质
var loader = new THREE.OBJLoader();
// 没有材质文件,系统自动设置Phong网格材质
loader.load('./立方体/box.obj',function (obj) {
// 控制台查看返回结构:包含一个网格模型Mesh的组Group
console.log(obj);
// 查看加载器生成的材质对象:MeshPhongMaterial
console.log(obj.children[0].material);
scene.add(obj);
// 加载后的一些编辑操作
obj.children[0].scale.set(20,20,20);//网格模型缩放
obj.children[0].geometry.center();//网格模型的几何体居中
obj.children[0].material.color.set(0xff0000);//设置材质颜色
})
- 同时加载obj和mtl文件
var OBJLoader = new THREE.OBJLoader();//obj加载器
var MTLLoader = new THREE.MTLLoader();//材质文件加载器
MTLLoader.load('./立方体/box.mtl', function(materials) {
// 返回一个包含材质的对象MaterialCreator
console.log(materials);
//obj的模型会和MaterialCreator包含的材质对应起来
OBJLoader.setMaterials(materials);
OBJLoader.load('./立方体/box.obj', function(obj) {
console.log(obj);
obj.scale.set(10, 10, 10); //放大obj组对象
scene.add(obj);//返回的组对象插入场景中
})
})
模型纹理贴图
obj模型的mtl文件可能包含纹理贴图,一个包含纹理贴图路径的.mtl
文件,如果路径有问题,可能会无法加载,可以仿照该案例修改。
// 一个包含纹理贴图路径的.mtl文件
newmtl material_1
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.9880 0.9880 0.9880
Ks 0.1200 0.1200 0.1200
map_Kd ./贴图/Earth.png
map_ks ./贴图/EarthSpec.png
norm ./贴图/EarthNormal.png
mtl和threejs贴图对应关系:
mtl贴图 | Threejs贴图 |
---|---|
map_kd | map 颜色贴图 |
map_ks | specularMap 高光贴图 |
norm | normalMap 法线贴图 |
map_bump/bump | bumpMap 凹凸贴图 |
3dmax导出的obj和mtl模型文件有时候需要修改一下个别位置字符,比如
.obj
中.mtl
文件的名称可能是乱码mtllib �����.mtl
,.mtl
文件中贴图的路径要设置正确,比如导出的是绝对路径,要改为相对路径。
4. 加载fbx并解析骨骼动画
stl、obj都是静态模型,不可以包含动画,fbx除了包含几何、材质信息,可以存储骨骼动画等数据。
解析之前可以先在浏览器控制台查看动画相关的数据是如何存储的。你可以看到obj.animations
属性的数组包含两个剪辑对象AnimationClip,obj.animations[0]
对应剪辑对象AnimationClip
包含多组关键帧KeyframeTrack数据,obj.animations[1]
对应的剪辑对象AnimationClip
没有关键帧数据,也就是说没有关键帧动画。
var mixer=null;//声明一个混合器变量
var loader = new THREE.FBXLoader();//创建一个FBX加载器
loader.load("SambaDancing.fbx", function(obj) {
// console.log(obj)
scene.add(obj)
obj.translateY(-80);
// obj作为参数创建一个混合器,解析播放obj及其子对象包含的动画数据
mixer = new THREE.AnimationMixer(obj);
// 查看动画数据
console.log(obj.animations)
// obj.animations[0]:获得剪辑对象clip
var AnimationAction=mixer.clipAction(obj.animations[0]);
// AnimationAction.timeScale = 1; //默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play();//播放动画
})
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
// 异步
if (mixer !== null) {
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
}
render();
十五、webgl渲染器
.domElement
属性是执行渲染方法.render()
的渲染结果,本质是一个HTML元素canvas.setSize()
方法,设置渲染结果的尺寸范围,本质上就是设置.domElement
表示的canvas画布的宽高尺寸
/**
* 相机设置
*/
// **与输出画布canvas的宽高比例保持一致**
var width = 300; //窗口宽度
var height = 300; //窗口高度
var k = width / height; //窗口宽高比
var s = 150; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
//透视投影相机设置
// var camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
/**
* 创建渲染器对象
*/
var renderer = new THREE.WebGLRenderer();
// 设置渲染区域尺寸,本质就是设置输出canvas的尺寸
renderer.setSize(width, height);
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
// 把渲染器的渲染结果canvas对象插入到对应的div元素中
document.getElementById('pos').appendChild(renderer.domElement);