threejs加载外部三维模型(gltf)

1. 建模软件绘制3D场景(Blender)

这节课主要给大家科普一些三维模型创建、美术和程序员协作的相关问题。

三维建模软件作用

对于简单的立方体、球体等模型,你可以通过three.js的几何体相关API快速实现,不过复杂的模型,比如一辆轿车、一栋房子、一个仓库,一般需要通过3D建模软件来实现。

三维建模软件简介

3D美术常用的三维建模软件,比如Blender、3dmax、C4D、maya等等

  • Blender(轻量开源)
  • 3dmax
  • C4D
  • maya

特殊行业项目可能涉及到行业软件,比如机械相关、建筑相关

  • 机械相关:SW、UG等
  • 建筑相关:草图大师、revit

分工和流程

3D美术:使用三维建模软件绘制3D模型,导出gltf等常见格式

程序:加载解析三维软件导出的三维模型

比如使用Blender三维建模软件导出gltf格式模型,然后再通过threejs加载三维模型。

程序员学习Blender好处

3D相关概念,相比较代码,建模软件,更加形象,容易理解
Blender与Threejs代码的交互,与美术更好的配合,Blender如何导出模型 

2. GLTF格式简介 (Web3D领域JPG)

gltf格式的重要性

GLTF格式是新2015发布的三维模型格式,随着物联网、WebGL、5G的进一步发展,会有越来越多的互联网项目Web端引入3D元素,你可以把GLTF格式的三维模型理解为.jpg、.png格式的图片一样,现在的网站,图片基本是标配,对于以后的网站来说如果需要展示一个场景,使用3D来替换图片表达也是很正常的事情。图片有很多格式,对于三维模型自然也是如此,Web开发的时候图片会有常用格式,对于Web3D开发也一样,肯定会根据需要选择一个常见的大家都熟悉的格式,随时时间的发展,GLTF必然称为一个极为重要的标准格式。

不仅three.js,其它的WebGL三维引擎cesium、babylonjs都对gltf格式有良好的的支持。

GLTF 2.0

Khronos Group组织2015发布了GLTF 1.0版本,在2017年又发布了GLTF2.0的版本。

关于glTF的更多介绍和信息,可以查看github:https://github.com/KhronosGroup/glTF

gltf包含内容

相比较obj、stl等格式而言,.gltf格式可以包含更多的模型信息。

.gltf格式文件几乎可以包含所有的三维模型相关信息的数据,比如模型层级关系、PBR材质、纹理贴图、骨骼动画、变形动画...

GLTF格式信息

如果你有一定的前端基础,那么你对JSON一定不陌生,GLTF文件就是通过JSON的键值对方式来表示模型信息,比如meshes表示网格模型信息,materials表示材质信息...

{
  "asset": {
    "version": "2.0",
  },
...
// 模型材质信息
  "materials": [
    {
      "pbrMetallicRoughness": {//PBR材质
        "baseColorFactor": [1,1,0,1],
        "metallicFactor": 0.5,//金属度
        "roughnessFactor": 1//粗糙度
      }
    }
  ],
  // 网格模型数据
  "meshes": ...
  // 纹理贴图
  "images": [
        {
            // uri指向外部图像文件
            "uri": "贴图名称.png"//图像数据也可以直接存储在.gltf文件中
        }
   ],
     "buffers": [
    // 一个buffer对应一个二进制数据块,可能是顶点位置 、顶点索引等数据
    {
      "byteLength": 840,
     //这里面的顶点数据,也快成单独以.bin文件的形式存在   
      "uri": "data:application/octet-stream;base64,AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/.......
    }
  ],
}

.bin文件

有些glTF文件会关联一个或多个.bin文件,.bin文件以二进制形式存储了模型的顶点数据等信息。 .bin文件中的信息其实就是对应gltf文件中的buffers属性,buffers.bin中的模型数据,可以存储在.gltf文件中,也可以单独一个二进制.bin文件。

"buffers": [
    {
        "byteLength": 102040,
        "uri": "文件名.bin"
    }
]

二进制.glb

gltf格式文件不一定就是以扩展名.gltf结尾,.glb就是gltf格式的二进制文件。比如你可以把.gltf模型和贴图信息全部合成得到一个.glb文件中,.glb文件相对.gltf文件体积更小,网络传输自然更快。

导出gltf

blender:最新版本可以直接导出gltf。

3damx gltf相关插件:https://github.com/BabylonJS/Exporters/releases

Blender导入导出gltf模型文件

你可以用Blender软件导出绘制好的三维模型,也可以打开和预览gltf格式文件模型。

3. 加载.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颜色空间

 model.js

// 引入three.js
import * as THREE from "three";

// 引入gltf加载器
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// 实例化一个加载器对象
const loader = new GLTFLoader()
const model = new THREE.Group()
loader.load('../../glTF/DamagedHelmet.gltf', function(gltf) {
  // console.log('gltf', gltf);
  model.add(gltf.scene)
})

export default model;

index.js

// 引入threejs
import * as THREE from "three";
// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import model from "./model.js";

// 创建一个三维场景scene
const scene = new THREE.Scene();
scene.add(model);

// 创建一个三维坐标轴
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper); //将坐标轴对象添加到三维场景中

// 创建一个辅助网格地面的效果
// const girdHelper = new THREE.GridHelper(600, 50, 0x00ffff,0x004444);
// scene.add(girdHelper);

// 创建一个光源对象  点光源
const pointLight = new THREE.PointLight(0xffffff, 1.0);
pointLight.decay = 0.0; //不随着距离的改变而衰减
pointLight.position.set(400, 200, 300); //偏移光源位置,观察渲染效果变化
// scene.add(pointLight); //点光源添加到场景中

//可视化点光源
// const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
// scene.add(pointLightHelper);
// 添加一个环境光
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient); //没有方向,也就没有立体的

// 删除
// scene.remove(ambient, model);
// scene.remove(ambient)
// scene.remove(model)

// 添加一个平行光
const directionalLight = new THREE.DirectionalLight(0xfffff, 0.8);
directionalLight.position.set(100, 100, 100); //棱角很弱,跟每个面的夹角都一样
directionalLight.position.set(100, 60, 50); //可以看出每个面的棱角不一样
// directionalLight.target = mesh; //默认坐标原点
scene.add(directionalLight);

// 定义相机输出画布的尺寸(单位:像素px)
const width = window.innerWidth;
const height = window.innerHeight;
// 设置相机的四个参数

// 创建一个透视投影相机对象
const camera = new THREE.PerspectiveCamera(10, width / height, 0.001, 600);
// 设置相机的位置
camera.position.set(10, 10, 10); //相机在Three.js三维坐标系中的位置
// 相机的视线,观察目标点的坐标
camera.lookAt(0, 0, 0); //坐标原点

// 创建一个WebGL渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true, //启用抗锯齿,线条更加流畅,减少锯齿状
});
renderer.setSize(width, height); //canvas画布的宽高度
renderer.render(scene, camera); //执行一个渲染操作,类比相机的拍照动作 咔
//把渲染结果canvas画布,也就是所谓的“照片”,添加到网页的页面上
document.body.appendChild(renderer.domElement);
// 插入到任意的html元素中
// document.getElementById("webgl").appendChild(renderer.domElement)

// 设置编码方式和gltf贴图保持一致,解决渲染颜色偏差的问题
renderer.outputEncoding = THREE.sRGBEncoding;

console.log("查看当前屏幕设备像素比", window.devicePixelRatio); //查看当前屏幕设备像素比 2
// 告诉threejs你的屏幕的设备像素比window.devicePixelRatio,针对与像素接近于1的设置下面的语句可能不是很明显,对于屏幕比例是2的,高清屏这种,设置的效果会很明显,减少模糊
renderer.setPixelRatio(window.devicePixelRatio); //会很清晰,遇到模糊了不要忘记设置这个
// renderer.setClearColor(0x444444);

// 渲染循环
function render() {
  // model.rotateY(0.01); //周期性旋转,每次旋转0.01弧度
  renderer.render(scene, camera); //周期性执行相机渲染功能,更新canvas画布上的内容
  requestAnimationFrame(render);
}
render();

// 创建一个相机控件对象
const controls = new OrbitControls(camera, renderer.domElement);
// controls.target.set(100, 0, 0); //默认为0,0,0
controls.update();
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener("change", function () {
  // console.log(camera.position);
  // 每当发生改变的时候就重新渲染
  renderer.render(scene, camera); //执行渲染操作
});

window.onresize = function () {
  // 更新canvas画布的尺寸
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 相机的视椎体宽高比一定和画布保持一致,否则物体就会扭曲
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
};

效果图如下:threejs源码案例当中的gltf文件渲染出来的结果

 

4. OrbitControls辅助设置相机参数

实际开发的时候,一方面可以通过OrbitControls旋转缩放预览3D模型,另一方面也可以辅助你选择合适的相机参。

OrbitControls改变相机位置.position

通过OrbitControls旋转和缩放,本质上就是在改变透视投影相机PerspectiveCamera的位置.position。

渲染循环中不停地打印相机的位置属性,你可以通过相机控件旋转或缩放三维场景,同时通过浏览器控制台观察相机位置变化。

function render() {
  requestAnimationFrame(render);
  // 浏览器控制台查看相机位置变化
  console.log('camera.position',camera.position);
}
render();

通过OrbitControls设置相机位置.position

上节课关于相机整体预览三维场景代码设置的时候,第一步是根据渲染范围的数量级,大概设置相机的位置参数,其实第二部,相机位置具体参数,可以借助OrbitControls可视化旋转或缩放,然后选择一个合适的渲染效果,浏览器控制台记录下此时的相机位置。

camera.position.set(200, 200, 200);//第1步:根据场景渲染范围尺寸设置
camera.position.set(-144, 95, 95); //第2步:通过相机控件辅助设置OrbitControls

OrbitControls改变相机.lookAt观察目标

通过OrbitControls平移,OrbitControls的.target属性会发生变化,.target属性对应的就是透视投影相机PerspectiveCamera的.lookAt观察目标`。

function render() {
  requestAnimationFrame(render);
  // 浏览器控制台查看controls.target变化,辅助设置lookAt参数
  console.log('controls.target',controls.target);
}
render();

通过OrbitControls设置.lookAt()参数

参照OrbitControls设置相机位置.position的过程,你可以平移三维场景,然后选择一个合适的渲染效果,记录下此时相机控件目标属性controls.target的值,然后作为透视投影相机.lookAt()的参数。

注意相机控件OrbitControls会影响lookAt设置,注意手动设置OrbitControls的目标参数

// camera.lookAt(0, 0, 0);
const x = -1.2,y = -15,z = 10;//通过OrbitControls辅助设置
camera.lookAt(x, y, z);

// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 相机控件.target属性在OrbitControls.js内部表示相机目标观察点,默认0,0,0
// console.log('controls.target', controls.target);
controls.target.set(x, y, z); //与lookAt参数保持一致
controls.update(); //update()函数内会执行camera.lookAt(controls.target)

index.js,练习代码

// 引入threejs
import * as THREE from "three";
// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import model from "./model.js";

// 创建一个三维场景scene
const scene = new THREE.Scene();
scene.add(model);

// 创建一个三维坐标轴
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper); //将坐标轴对象添加到三维场景中

// 创建一个辅助网格地面的效果
// const girdHelper = new THREE.GridHelper(600, 50, 0x00ffff,0x004444);
// scene.add(girdHelper);

// 创建一个光源对象  点光源
const pointLight = new THREE.PointLight(0xffffff, 1.0);
pointLight.decay = 0.0; //不随着距离的改变而衰减
pointLight.position.set(400, 200, 300); //偏移光源位置,观察渲染效果变化
// scene.add(pointLight); //点光源添加到场景中

//可视化点光源
// const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
// scene.add(pointLightHelper);
// 添加一个环境光
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient); //没有方向,也就没有立体的

// 删除
// scene.remove(ambient, model);
// scene.remove(ambient)
// scene.remove(model)

// 添加一个平行光
const directionalLight = new THREE.DirectionalLight(0xfffff, 0.8);
directionalLight.position.set(100, 100, 100); //棱角很弱,跟每个面的夹角都一样
directionalLight.position.set(100, 60, 50); //可以看出每个面的棱角不一样
// directionalLight.target = mesh; //默认坐标原点
scene.add(directionalLight);

// 定义相机输出画布的尺寸(单位:像素px)
const width = window.innerWidth;
const height = window.innerHeight;
// 设置相机的四个参数

// 创建一个透视投影相机对象
const camera = new THREE.PerspectiveCamera(10, width / height, 0.001, 600);
// 设置相机的位置
camera.position.set(10, 10, 10); //相机在Three.js三维坐标系中的位置
camera.position.set(20, 8, 9); //根据相机可视化调试camera.position
// 相机的视线,观察目标点的坐标
// camera.lookAt(0, 0, 0); //坐标原点
camera.lookAt(0.5, -0.15, 0.3);
// 创建一个WebGL渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true, //启用抗锯齿,线条更加流畅,减少锯齿状
});
renderer.setSize(width, height); //canvas画布的宽高度
renderer.render(scene, camera); //执行一个渲染操作,类比相机的拍照动作 咔
//把渲染结果canvas画布,也就是所谓的“照片”,添加到网页的页面上
document.body.appendChild(renderer.domElement);
// 插入到任意的html元素中
// document.getElementById("webgl").appendChild(renderer.domElement)

// 设置编码方式和gltf贴图保持一致,解决渲染颜色偏差的问题
renderer.outputEncoding = THREE.sRGBEncoding;

console.log("查看当前屏幕设备像素比", window.devicePixelRatio); //查看当前屏幕设备像素比 2
// 告诉threejs你的屏幕的设备像素比window.devicePixelRatio,针对与像素接近于1的设置下面的语句可能不是很明显,对于屏幕比例是2的,高清屏这种,设置的效果会很明显,减少模糊
renderer.setPixelRatio(window.devicePixelRatio); //会很清晰,遇到模糊了不要忘记设置这个
// renderer.setClearColor(0x444444);

// 创建一个相机控件对象
const controls = new OrbitControls(camera, renderer.domElement);
// 渲染循环
function render() {
  // console.log("camera.position", camera.position);
  console.log("controls.target", controls.target);
  // model.rotateY(0.01); //周期性旋转,每次旋转0.01弧度
  renderer.render(scene, camera); //周期性执行相机渲染功能,更新canvas画布上的内容
  requestAnimationFrame(render);
}
render();

controls.target.set(0.5, -0.15, 0.3); //默认为0,0,0,所以更改值之后要注意更新,并且与lookAt的参数一致
controls.update();
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener("change", function () {
  // console.log(camera.position);
  // 每当发生改变的时候就重新渲染
  renderer.render(scene, camera); //执行渲染操作
});

window.onresize = function () {
  // 更新canvas画布的尺寸
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 相机的视椎体宽高比一定和画布保持一致,否则物体就会扭曲
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
};

5. gltf不同文件形式(.glb)

.gltf格式模型文件,有不同的组织形式。

单独.gltf文件
单独.glb文件
.gltf + .bin + 贴图文件
这些不同形式的gltf模型,加载代码其实没啥区别。

// 单独.gltf文件
loader.load("../../工厂.gltf", function (gltf) { 
    scene.add(gltf.scene);
})

lender导出gltf不同形式

Blender三维建模软件,可以根据设置,以不同形式导出gltf模型,比如单独导出一个.gltf文件,比如单独导出一个.glb文件,比如导出形式为.gltf + .bin + 贴图多个文件

.glb文件

.glb是gltf格式的二进制形式文件,加载方式和.gltf没啥区别。

// 单独.glb文件
loader.load("../../工厂.glb", function (gltf) { 
    scene.add(gltf.scene);
})

.gltf + .bin + 贴图文件

gltf模型的一些数据,是可以以单独文件形式存在的,比如纹理贴图单独存在,比如.bin包含gltf的顶点数据。

要注意的就是贴图等数据单独是一个文件的时候,注意不随随意改变子文件相对父文件gltf的目录,以免找不到资源。

// .gltf + .bin + 贴图文件
loader.load("../../工厂/工厂.gltf", function (gltf) { 
    scene.add(gltf.scene);
})

 代码:

// 引入three.js
import * as THREE from "three";

// 引入gltf加载器
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// 实例化一个加载器对象
const loader = new GLTFLoader()
const model = new THREE.Group()
// loader.load('../../glTF/DamagedHelmet.gltf', function(gltf) {
//   // console.log('gltf', gltf);
//   model.add(gltf.scene)
// })

// 加载glb格式的gltf模型
loader.load('../../ClearcoatTest.glb', function(gltf) {
  // console.log('gltf', gltf);
  model.add(gltf.scene)
})

export default model;

6. 模型命名(程序与美术协作)

开发一些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);
})

7. 递归遍历层级模型修改材质

加载一个外部模型,比如gltf模型,如果你想批量修改每个Mesh的材质,一个一个设置比较麻烦,可以通过递归遍历方法.traverse()批量操作更加方便。

递归遍历方法.traverse()

递归遍历gltf所有的模型节点。

// 递归遍历所有模型节点批量修改材质
gltf.scene.traverse(function(obj) {
    if (obj.isMesh) {//判断是否是网格模型
        console.log('模型节点',obj);
        console.log('模型节点名字',obj.name);
    }
});

查看gltf默认的材质

.obj、.gltf、.fbx等不同格式的模型,threejs加载默认的材质可能不同,不过也不用刻意记忆,通过浏览器控制台log打印即可console.log(obj.material)。

threejs解析gltf模型默认材质一般是MeshStandardMaterial或MeshPhysicalMaterial,相比较其它网格材质,这两个材质属于PBR物理材质,可以提供更加真实的材质效果

// 递归遍历所有模型节点批量修改材质
gltf.scene.traverse(function(obj) {
    if (obj.isMesh) {
        console.log('gltf默认材质',obj.material);
    }
});

批量修改gltf所有Mesh的材质

gltf.scene.traverse(function(obj) {
    if (obj.isMesh) {
        // 重新设置材质
        obj.material = new THREE.MeshLambertMaterial({
            color:0xffffff,
        });
    }
});

相关代码:

// 引入three.js
import * as THREE from "three";

// 引入gltf加载器
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// 实例化一个加载器对象
const loader = new GLTFLoader();
const model = new THREE.Group();

// 加载glb格式的gltf模型
// loader.load("../../ClearcoatTest.glb", function (gltf) {
//   // console.log('gltf', gltf);
//   // model.add(gltf.scene)
//   gltf.scene.traverse(function (obj) {
//     // 只获取所有mesh节点
//     if (obj.isMesh) {
//       // 查看threejs渲染gltf默认材质
//       console.log("obj", obj.material);
//     }
//   });
//   model.add(gltf.scene);
// });


loader.load('../../glTF/DamagedHelmet.gltf', function(gltf) {
  // gltf加载成功后返回一个对象
  // console.log('控制台查看gltf对象结构', gltf)
  gltf.scene.traverse(function(obj){
    // 只获取所有mesh节点
    if(obj.isMesh){
      obj.material = new THREE.MeshLambertMaterial({
        color: 0xffffff
      })
    }
  })
  // console.log('场景3D模型数据', gltf.scene)
  // console.log('gltf', gltf);
  model.add(gltf.scene) //三维场景添加到model组对象中
})

export default model;

 

8. 外部模型材质是否共享的问题

美术通过三维建模软件,比如Blender绘制好一个三维场景以后,一些外观一样的Mesh,可能会共享一个材质对象。

改变一个模型颜色其它模型跟着变化

由于楼房的Mesh共享了1号楼Mesh的材质,当你通过mesh1.material改变mesh1材质,本质上是改变所有楼Mesh的材质。

const mesh1 = gltf.scene.getObjectByName("1号楼");
//1. 改变1号楼Mesh材质颜色
mesh1.material.color.set(0xff0000);

.name标记材质,判断两个mesh是否共享材质

通过.name标记材质,测试mesh1和mesh2是否共享了材质

const mesh1 = gltf.scene.getObjectByName("1号楼");
mesh1.material.name = '楼房材质';//通过name标记mesh1对应材质
const mesh2 = gltf.scene.getObjectByName("2号楼");
//通过name相同,可以判断mesh1.material和mesh2.material共享了同一个材质对象
console.log('mesh2.material.name', mesh2.material.name);

解决问题方向

改变一个模型颜色其它模型跟着变化,是因为多个模型对象共享了材质,如果单独改变一个模型的材质,比如颜色,下面两个方案,可以任选其一。

三维建模软件中设置,需要代码改变材质的Mesh不要共享材质,要独享材质。
代码批量更改:克隆材质对象,重新赋值给mesh的材质属性

代码方式解决多个mesh共享材质的问题

//用代码方式解决mesh共享材质问题
gltf.scene.getObjectByName("小区房子").traverse(function (obj) {
    if (obj.isMesh) {
        // .material.clone()返回一个新材质对象,和原来一样,重新赋值给.material属性
        obj.material = obj.material.clone();
    }
});
mesh1.material.color.set(0xffff00);
mesh2.material.color.set(0x00ff00);

相关代码:

// 引入three.js
import * as THREE from "three";

// 引入gltf加载器
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// 实例化一个加载器对象
const loader = new GLTFLoader();
const model = new THREE.Group(); //声明一个组对象,用来添加加载成功的三维场景

// 加载glb格式的gltf模型
loader.load("../../ClearcoatTest.glb", function (gltf) {
  // console.log('gltf', gltf);
  // model.add(gltf.scene)
  const mesh1 = gltf.scene.getObjectByName("R3_CoatOnlySample");
  console.log(mesh1, "22222");
  mesh1.material.name = "楼层材质";
  const mesh2 = gltf.scene.getObjectByName("R2_CoatOnlySample");
  console.log("mesh2.material.name", mesh2.material.name)

  //用代码方式解决mesh共享材质问题
  gltf.scene.getObjectByName("R1_CoatOnlySample").traverse(function (obj) {
    if (obj.isMesh) {
      // .material.clone()返回一个新材质对象,和原来一样,重新赋值给.material属性
      obj.material = obj.material.clone();
    }
  });
  mesh1.material.color.set(0xffff00);
  mesh2.material.color.set(0x00ff00);
  model.add(gltf.scene);
});

export default model;

9. 纹理encoding和渲染器

如果没有特殊需要,一般为了正常渲染,避免颜色偏差,threejs代码中需要颜色贴图.encoding和渲染器.outputEncoding属性值保持一致。

纹理对象Texture颜色空间编码属性.encoding

纹理对象Texture颜色空间 (opens new window)编码属性.encoding有多个属性值,默认值是线性颜色空间THREE.LinearEncoding。

THREE.LinearEncoding:线性颜色空间
THREE.sRGBEncoding:sRGB (opens new window)颜色空间

浏览器控制台查看Texture.encoding属性值

const texture = new THREE.TextureLoader().load('./earth.jpg');
texture.encoding = THREE.LinearEncoding;//默认值
// THREE.LinearEncoding变量在threejs内部表示数字3000
console.log('texture.encoding',texture.encoding);
// 修改为THREE.sRGBEncoding,
texture.encoding = THREE.sRGBEncoding;
// THREE.sRGBEncoding变量在threejs内部表示数字3001
console.log('texture.encoding',texture.encoding);

THREE.LinearEncoding、THREE.sRGBEncoding其实在theeejs内部都表示一个数字,具体可以查看src目录下constants.js的源码文件。

// constants.js源码部分截取
export const LinearEncoding = 3000;
export const sRGBEncoding = 3001;

gltfmap.encoding值

threejs加载gltf模型,颜色贴图map属性.encoding的默认值是sRGB颜色空间THREE.sRGBEncoding

// 查看gltf所有颜色贴图的.encoding值
gltf.scene.traverse(function(obj) {
    if (obj.isMesh) {
        if(obj.material.map){//判断是否存在贴图
            console.log('.encoding',obj.material.map.encoding);
        }
    }
});
// .encoding显示3001,说明是THREE.sRGBEncoding
console.log('.encoding',mesh.material.map.encoding);

WebGL渲染器.outputEncoding

.outputEncoding的默认值是线性空间THREE.LinearEncoding,和纹理对象.encoding默认值一样,如果颜色贴图.encoding的值是THREE.sRGBEncoding,为了避免颜色偏差,.outputEncoding的值也需要设置为THREE.sRGBEncoding。

//解决加载gltf格式模型颜色偏差问题
renderer.outputEncoding = THREE.sRGBEncoding;

注意!最新版本属性名字有改变。渲染器属性名.outputEncoding已经变更为.outputColorSpace,具体参考6.3小节最后说明。

单独加载的颜色贴图设置.encoding = THREE.sRGBEncoding

如果webgl渲染器设置了renderer.outputEncoding = THREE.sRGBEncoding;,你单独加载图像返回的纹理对象需要设置 texture.encoding = THREE.sRGBEncoding;

//解决加载gltf格式模型颜色偏差问题
renderer.outputEncoding = THREE.sRGBEncoding;
const texture = new THREE.TextureLoader().load('./earth.jpg');
// 和webgl渲染器renderer.outputEncoding一致
texture.encoding = THREE.sRGBEncoding;

注意!!!最新版本,纹理对象属性名.encoding已经变更为.colorSpace。

texture.colorSpace  = THREE.SRGBColorSpace;//设置为SRGB颜色空间

 10. gltf模型更换.map(纹理.flipY)

下面给大家演示如何给gltf的网格模型Mesh更换颜色贴图.map

加载颜色贴图.map

注意单独加载的纹理贴图的.encoding和webgl渲染器的.outputEncoding保持一致。

const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./黑色.png');// 加载手机mesh另一个颜色贴图
texture.encoding = THREE.sRGBEncoding; //和渲染器.outputEncoding一样值

更换gltf颜色贴图

执行mesh.material.map = texture;新的纹理对象Texture赋值给.material.map就可以更换材质贴图。

loader.load("../手机模型.glb", function (gltf) {
    const mesh = gltf.scene.children[0]; //获取Mesh
    mesh.material.map = texture; //更换不同风格的颜色贴图
})

注意:如果你直接给gltf模型材质设置.map属性更换贴图,会出现纹理贴图错位的问题,这主要和纹理对象Texture的翻转属性.flipY有关。

纹理对象Texture翻转属性.flipY默认值

.flipY表示是否翻转纹理贴图在Mesh上的显示位置。

纹理对象Texture翻转属性.flipY默认值是true。

// 纹理对象texture.flipY默认值
console.log('texture.flipY', texture.flipY);

gltf的贴图翻转属性.flipY默认值

gltf的贴图翻转属性.flipY默认值是false。

loader.load("../手机模型.glb", function (gltf) {
    const mesh = gltf.scene.children[0]; //获取Mesh
    console.log('.flipY', mesh.material.map.flipY);
})

如果更换单独加载的纹理贴图,比如颜色贴图.map,注意把纹理贴图.flipY的值设置给gltf中纹理的值false。

//是否翻转纹理贴图
texture.flipY = false;

### 关于 UniApp 框架推荐资源与教程 #### 1. **Uniapp 官方文档** 官方文档是最权威的学习资料之一,涵盖了从基础概念到高级特性的全方位讲解。对于初学者来说,这是了解 UniApp 架构技术细节的最佳起点[^3]。 #### 2. **《Uniapp 从入门到精通:案例分析与最佳实践》** 该文章提供了系统的知识体系,帮助开发者掌握 Uniapp 的基础知识、实际应用以及开发过程中的最佳实践方法。它不仅适合新手快速上手,也能够为有经验的开发者提供深入的技术指导[^1]。 #### 3. **ThorUI-uniapp 开源项目教程** 这是一个专注于 UI 组件库设计实现的教学材料,基于 ThorUI 提供了一系列实用的功能模块。通过学习此开源项目的具体实现方式,可以更好地理解如何高效构建美观且一致的应用界面[^2]。 #### 4. **跨平台开发利器:UniApp 全面解析与实践指南** 这篇文章按照章节形式详细阐述了 UniApp 的各个方面,包括但不限于其工作原理、技术栈介绍、开发环境配置等内容,并附带丰富的实例演示来辅助说明理论知识点。 以下是几个重要的主题摘选: - **核心特性解析**:解释了跨端运行机制、底层架构组成及其主要功能特点。 - **开发实践指南**:给出了具体的页面编写样例代码,展示了不同设备间 API 调用的方法论。 - **性能优化建议**:针对启动时间缩短、图形绘制效率提升等方面提出了可行策略。 ```javascript // 示例代码片段展示条件编译语法 export default { methods: { showPlatform() { console.log(process.env.UNI_PLATFORM); // 输出当前平台名称 #ifdef APP-PLUS console.log('Running on App'); #endif #ifdef H5 console.log('Running on Web'); #endif } } } ``` #### 5. **其他补充资源** 除了上述提到的内容外,还有许多在线课程视频可供选择,比如 Bilibili 上的一些免费系列讲座;另外 GitHub GitCode 平台上也有不少优质的社区贡献作品值得借鉴研究。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值