Three.js衣服logo文字定制
3D在线体验地址:http://www.webgl3d.cn/3D/yifudingzhi/index.html#/home
视频讲解思路:https://www.bilibili.com/video/BV1oN4y1H7F3/
参考资料:Threejs中文网
美术
首先准备一个衣服的3D模型,比如公司美术Blender绘制,当然也可以用别的建模软件,我为了省事,就花费花钱买的别的美术的模型。
美术与程序交接
衣服上放置logo或文字的区域,紧贴着衣服创建一个曲面,用来添加文字或logo
获取网格模型
加载gltf模型之后,可以通过模型节点名称,获取模型对象。
const logo = twin.model.getObjectByName('logo');
const mesh = this.model.getObjectByName('文字')
更换衣服logo
可以通过input加载图片对象
<div class="file-upload">
<input ref="imageInput" id="imageInput" type="file" accept="img/*">
<label for="imageInput">上传logo</label>
</div>
加载图片文件数据,生成img对象,然后创建纹理贴图,赋值给logo对mesh的贴图
const logo = twin.model.getObjectByName('logo');
const imgObj = new Image();
imgObj.src = imageDataUrl;
const texture = new THREE.CanvasTexture(imgObj);
texture.flipY =false
texture.colorSpace = THREE.SRGBColorSpace;//设置为SRGB颜色空间
// document.body.appendChild(imgObj)
const obj = twin.model.getObjectByName('logo');
obj.material.map = texture
自定义文字
自定义文字和自定义logo基本思路一样,区别在于需要通过canvas画布生成文字。
//canvas是带有文字的canvas画布对象
const texture = new THREE.CanvasTexture(canvas);
texture.flipY = false
const mesh = this.model.getObjectByName('文字')
mesh.material.transparent=true
mesh.material.map = texture;
扩展threejs知识点:
如果你对threejs基础的知识点,不太了解,可以查看下面介绍,如果比较熟悉,就不用看了,直接查看上面思路即可。
创建纹理贴图
通过纹理贴图加载器TextureLoader
的load()
方法加载一张图片可以返回一个纹理对象Texture
,纹理对象Texture
可以作为模型材质颜色贴图.map
属性的值。
const geometry = new THREE.PlaneGeometry(200, 100);
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
// .load()方法加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./earth.jpg');
const material = new THREE.MeshLambertMaterial({
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture,//map表示材质的颜色贴图属性
});
颜色贴图属性.map
也可以通过颜色贴图属性.map
直接设置纹理贴图,和材质的参数设置一样。
material.map = texture;
颜色贴图和color属性颜色值会混合
材质的颜色贴图属性.map
设置后,模型会从纹理贴图上采集像素值,这时候一般来说不需要再设置材质颜色.color
。.map
贴图之所以称之为颜色贴图就是因为网格模型会获得颜色贴图的颜色值RGB。
颜色贴图map和color属性颜色值会混合。如果没有特殊需要,设置了颜色贴图.map,不用设置color的值,color默认白色0xffffff。
const material = new THREE.MeshLambertMaterial({
// color: 0x00ffff,
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture,//map表示材质的颜色贴图属性
});
测试不同几何体添加纹理贴图的效果
你可以尝试把颜色纹理贴图映射到不同的几何体上查看渲染效果,至于为什么映射效果不同,其实和UV坐标相关,具体可以关注下节课关于UV坐标的讲解。
const geometry = new THREE.BoxGeometry(100, 100, 100); //长方体
const geometry = new THREE.SphereGeometry(60, 25, 25); //球体
加载.gltf文件(模型加载全流程)
本节课,以gltf格式为例,给大家讲解加载外部三维模型的整个过程。
场景、光源、渲染器、相机控件等前面说过基础代码,本节课不专门讲解,主要是把下面三部,给大家全流程演示一遍。
- gltf模型加载器
GLTFLoader.js
- 相机参数根据需要设置
- 加载gltf的时候,webgl渲染器编码方式设置
1.1.引入GLTFLoader.js
你在three.js官方文件的**examples/jsm/子文件loaders/**目录下,可以找到一个文件GLTFLoader.js
,这个文件就是three.js的一个扩展库,专门用来加载gltf格式模型加载器。
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
1.2.gltf加载器new GLTFLoader()
执行new GLTFLoader()
就可以实例化一个gltf的加载器对象。
// 创建GLTF加载器对象
const loader = new GLTFLoader();
1.3.gltf加载器方法.load()
通过gltf加载器方法.load()
就可以加载外部的gltf模型。
执行方法.load()
会返回一个gltf对象,作为参数2函数的参数,改gltf对象可以包含模型、动画等信息,本节课你只需要先了解gltf的场景属性gltf.scene
,该属性包含的是模型信息,比如几何体BufferGometry、材质Material、网格模型Mesh。
loader.load( 'gltf模型.gltf', function ( gltf ) {
console.log('控制台查看加载gltf文件返回的对象结构',gltf);
console.log('gltf对象场景属性',gltf.scene);
// 返回的场景对象gltf.scene插入到threejs场景中
scene.add( gltf.scene );
})
相机选择(正投影OrthographicCamera
和透视投影PerspectiveCamera
)
如果你想预览一个三维场景,一般有正投影相机OrthographicCamera
和透视投影相机PerspectiveCamera
可供选择。不过大部分3D项目,比如一般都是使用透视投影相机PerspectiveCamera
,比如游戏、物联网等项目都会选择透视投影相机PerspectiveCamera
。
如果你希望渲染的结果符合人眼的远小近大的规律,毫无疑问要选择透视投影相机,如果不需要模拟人眼远小近大的投影规律,可以选择正投影相机。
尺寸概念
项目开发的时候,程序员对一个模型或者说一个三维场景要有一个尺寸的概念,不用具体值,要有一个大概印象。
一般通过三维建模软件可以轻松测试测量模型尺寸,比如作为程序员你可以用三维建模软件blender打开gltf模型,测量尺寸。
单位问题
three.js的世界并没有任何单位,只有数字大小的运算。
obj、gltf格式的模型信息只有尺寸,并不含单位信息。
不过实际项目开发的时候,一般会定义一个单位,一方面甲方、前端、美术之间更好协调,甚至你自己写代码也要有一个尺寸标准。比如一个园区、工厂,可以m为单位建模,比如建筑、人、相机都用m为尺度去衡量,如果单位不统一,就需要你写代码,通过.scale
属性去缩放。
设置合适的相机参数
通过gltf加载完成,模型后,你还需要根据自身需要,设置合适的相机参数,就好比你拍照,你想拍摄一个石头,肯定要把相机对着石头,如果希望石头在照片上占比大,就要离石头近一些。
相机位置怎么设置,你就类比你的眼睛,如果你想模拟人在3D场景中漫游,那么很简单,你把相机放在地面上,距离地面高度和人身高接近即可。
如果你想看到工厂的全貌,你可以理解为你坐着无人机向下俯瞰,简单说,相比人漫游工厂,整体预览工厂相机距离工厂距离更远一些,否则你也看不到全貌,当然过于远了,你就看不清工厂了。
以课程工厂为例,先设定一个小目标,我们希望工厂能够居中显示在canvas画布上,并且保证可以整体预览。
下面以透视投影相机PerspectiveCamera
为例说明。
2.1.相机位置.position
工厂尺寸范围大概200米数量级,那么如果想整体预览观察工厂所有模型,那很简单,第一步,把camera.position
的xyz值统统设置为几百即可,比如(200, 200, 200)
。
具体xyz值,你可以通过OrbitControls可视化操作调整,然后浏览器控制台记录相机参数即可。
camera.position.set(200, 200, 200);
2.2 某位置在canvas画布居中
你需要工厂那个位置在canavs画布上居中,直接把camera.lookAt()
指向哪个坐标。
如果美术建模,把工厂整体居中,也就是说模型的几何中心,大概位于世界坐标原点。你设置camera.lookAt(0,0,0)
,相机视线指向坐标原点。
camera.lookAt(0, 0, 0);
注意相机控件OrbitControls会影响lookAt设置,注意手动设置OrbitControls的目标参数
camera.lookAt(100, 0, 0);
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 相机控件.target属性在OrbitControls.js内部表示相机目标观察点,默认0,0,0
// console.log('controls.target', controls.target);
controls.target.set(100, 0, 0);
controls.update();//update()函数内会执行camera.lookAt(controls.targe)
2.3.远裁截面far
参数
近裁截面near和远裁截面far,要能包含你想渲染的场景,否则超出视锥体模型会被剪裁掉,简单说near足够小,far足够大,主要是far。
PerspectiveCamera(fov, aspect, near, far)
测量工厂尺寸大概几百的数量级,这里不用测具体尺寸,有个大概数量级即可,然后far设置为3000足够了。
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
3.纹理贴图颜色偏差解决
three.js加载gltf模型的时候,可能会遇到three.js渲染结果颜色偏差,对于这种情况,你只需要修改WebGL渲染器默认的编码方式.outputEncoding
即可
//解决加载gltf格式模型纹理贴图和原图不一样问题
renderer.outputEncoding = THREE.sRGBEncoding;
注意!!!!!!!最新版本属性名字有改变。渲染器属性名.outputEncoding
已经变更为.outputColorSpace
。
查WebGL渲染器文档,你可以看到.outputColorSpace
的默认值就是SRGB颜色空间THREE.SRGBColorSpace
,意味着新版本代码中,加载gltf,没有特殊需要,不设置.outputColorSpace
也不会引起色差。
//新版本,加载gltf,不需要执行下面代码解决颜色偏差
renderer.outputColorSpace = THREE.SRGBColorSpace;//设置为SRGB颜色空间
模型命名(程序与美术协作)—层级模型节点选择
开发一些web3d项目,比如一个小区、工厂的可视化,场景中会有多个模型对象,程序员加载三维模型的时候,通过什么方式才能获取到自己想要的某个模型节点是个问题。
三维软件模型命名
课程提供了一个Blender的模型例子,你可以打开查看。
其实模型节点命名可以类比前后端API接口命名,web3d前端和后端对接需要命名接口,和3D美术对接,同样需要给一些模型节点命名。
-
模型命名可以使用汉字、英文、拼音其他语言形式。
-
如果使用汉字注意,有些三维建模软件可能存在导出乱码问题。
浏览器控制台查看3D模型树结构
加载gltf模型,通过gltf.scene
可以获取模型的数据,你可以通过浏览器控制打印gltf.scene
,然后和你三维建模软件中的模型目录树对比,比较两者的结构是否相同。
- 模型父对象节点可以用
Object3D
对象表示,也可以用组对象Group
表示。 - 通过
.children
属性可以查看一个父对象模型的的所有子对象。 - 通过
.name
属性可以查看模型节点的名称
loader.load("./简易小区.glb", function (gltf) {
console.log('场景3D模型树结构', gltf.scene);
model.add(gltf.scene);
})
.getObjectByName()
根据.name
获取模型节点
一般三维建模软件的目录树,都有模型的名称,three.js加载外部模型,外部模型的名称体现为three.js对象的.name
属性,three.js可以通过.getObjectByName()
方法,把模型节点的名字.name
作为改函数参数,快速查找某个模型对象。
// 返回名.name为"1号楼"对应的对象
const nameNode = gltf.scene.getObjectByName("1号楼");
nameNode.material.color.set(0xff0000);//改变1号楼Mesh材质颜色
分组管理
对于大类,可以进行分组,这样更好管理,比如高层分为一组,洋房分为一组。如果这样做的好处是,程序员可以通过分类名称,快速获取所有模型,然后进行同样的渲染操作,比如洋房批量改变颜色。
//获得所有'洋房'房子的父对象
const obj = gltf.scene.getObjectByName('洋房');
console.log('obj', obj); //控制台查看返回结果
console.log('obj.children', obj.children);
// obj.children的所有子对象都是Mesh,改变Mesh对应颜色
obj.children.forEach(function (mesh) {
mesh.material.color.set(0xffff00);
})
Sprite标签(Canvas作为贴图)
上节课案例创建标签的方式,是把一张图片作为Sprite
精灵模型的颜色贴图,本节给大家演示把Canvas画布作为Sprite
精灵模型的颜色贴图,实现一个标签。
注意:本节课主要是技术方案讲解,默认你有Canvas基础,如果没有Canvas基础,可以学习之后再来学习本节课内容。
Canvas画布绘制一个标签
你可以使用Canvas绘制特定轮廓的标签,比如加上指引线或箭头,可以输入特定文字。
下面代码自动适配了不同长度的文字标注,文字符号越多,canvas画布越长。
// 生成一个canvas对象,标注文字为参数name
function createCanvas(name) {
/**
* 创建一个canvas对象,绘制几何图案或添加文字
*/
const canvas = document.createElement("canvas");
const arr = name.split(""); //分割为单独字符串
let num = 0;
const reg = /[\u4e00-\u9fa5]/;
for (let i = 0; i < arr.length; i++) {
if (reg.test(arr[i])) { //判断是不是汉字
num += 1;
} else {
num += 0.5; //英文字母或数字累加0.5
}
}
// 根据字符串符号类型和数量、文字font-size大小来设置canvas画布宽高度
const h = 80; //根据渲染像素大小设置,过大性能差,过小不清晰
const w = h + num * 32;
canvas.width = w;
canvas.height = h;
const h1 = h * 0.8;
const c = canvas.getContext('2d');
// 定义轮廓颜色,黑色半透明
c.fillStyle = "rgba(0,0,0,0.5)";
// 绘制半圆+矩形轮廓
const R = h1 / 2;
c.arc(R, R, R, -Math.PI / 2, Math.PI / 2, true); //顺时针半圆
c.arc(w - R, R, R, Math.PI / 2, -Math.PI / 2, true); //顺时针半圆
c.fill();
// 绘制箭头
c.beginPath();
const h2 = h - h1;
c.moveTo(w / 2 - h2 * 0.6, h1);
c.lineTo(w / 2 + h2 * 0.6, h1);
c.lineTo(w / 2, h);
c.fill();
// 文字
c.beginPath();
c.translate(w / 2, h1 / 2);
c.fillStyle = "#ffffff"; //文本填充颜色
c.font = "normal 32px 宋体"; //字体样式设置
c.textBaseline = "middle"; //文本与fillText定义的纵坐标
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
c.fillText(name, 0, 0);
return canvas;
}
const canvas = createCanvas('设备A')
CanvasTexture
把canvas转化为纹理对象
canvas
画布作为CanvasTexture
的参数创建一个纹理对象,本质上你可以理解为CanvasTexture
把canvas画布当做图片,读取参数canvas画布上的像素值,创建纹理贴图Texture
。
loader.load("../工厂.glb", function (gltf) {
model.add(gltf.scene);
const canvas = createCanvas('设备A');//创建一个canvas画布
// canvas画布作为CanvasTexture的参数创建一个纹理对象
// 本质上你可以理解为CanvasTexture读取参数canvas画布上的像素值
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
});
const sprite = new THREE.Sprite(spriteMaterial);
})
精灵模型尺寸和位置设置
精灵模型尺寸和位置设置具体思路可以参考上节课讲解。
注意精灵模型宽高比和canvas画布宽高比保持一致即可。
const y = 4;//精灵y方向尺寸
// sprite宽高比和canvas画布保持一致
const x = canvas.width/canvas.height*y;//精灵x方向尺寸
sprite.scale.set(x, y, 1);// 控制精灵大小
sprite.position.y = y / 2; //标签底部箭头和空对象标注点重合
const obj = gltf.scene.getObjectByName('设备A标注'); // obj是建模软件中创建的一个空对象
obj.add(sprite); //tag会标注在空对象obj对应的位置
cavnas精灵标签封装(标注多个)
封装一个创建cavnas精灵标签的函数,可以根据需要调用,标注任何需要标注的地方。
import * as THREE from 'three';
import createCanvas from './canvas';
// 标注位置对应的模型对象obj
// name:标注文字
function createSprite(obj,name) {
const canvas = createCanvas(name);//创建一个canvas画布
// canvas画布作为CanvasTexture的参数创建一个纹理对象
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
});
const sprite = new THREE.Sprite(spriteMaterial);
// 控制精灵大小(sprite宽高比和canvas画布保持一致)
const s = 0.05;//通过canvas宽高度缩放后,设置sprite.scale,避免图文宽高比变形
const x = canvas.width*s;
const y = canvas.height*s;
sprite.scale.set(x, y, 1);
sprite.position.y = y / 2; //标签底部箭头和空对象标注点重合
obj.add(sprite); //tag会标注在空对象obj对应的位置
}
export default createSprite;
Canvas包含外部图片
如果Canvas包含外部图片作为背景,注意创建CanvasTexture
的时候,不管你的代码结构怎么组织,主要要等图像加载完成再执行THREE.CanvasTexture(canvas)
,如果还未加载完成,创建纹理时候,读取画布像素时候,会不包含图片。
// 生成一个canvas对象,标注文字为参数name
function createCanvas(img,name) {
/**
* 创建一个canvas对象,绘制几何图案或添加文字
*/
const canvas = document.createElement("canvas");
const w = 140; //根据渲染像素大小设置,过大性能差,过小不清晰
const h = 80;
canvas.width = w;
canvas.height = h;
const h1 = h * 0.8;
const c = canvas.getContext('2d');
c.fillStyle = "rgba(0,0,0,0.0)"; //背景透明
c.fillRect(0, 0, w, h);
c.drawImage(img, 0, 0, w, h);//图片绘制到canvas画布上
// 文字
c.beginPath();
c.translate(w / 2, h1 / 2);
c.fillStyle = "#ffffff"; //文本填充颜色
c.font = "normal 32px 宋体"; //字体样式设置
c.textBaseline = "middle"; //文本与fillText定义的纵坐标
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
c.fillText(name, 0, 0);
return canvas;
}
const img = new Image();
img.src = "./标签箭头背景.png";
img.onload = function () {
const canvas = createCanvas(img,'设备A');//创建一个canvas画布
// 图片加载完成后,读取canvas像素数据创建CanvasTexture
const texture = new THREE.CanvasTexture(canvas);
...
const sprite = new THREE.Sprite(spriteMaterial);
...
}