23.Three.js 解决方案之加载.obj 模型
23 Three.js 解决方案之加载.obj 模型
本文将开始
有
- 讲解内容时尽量简要,不再像前面
01-22 篇章那样啰嗦 - 和原版官方教程内容上略有不同,删除我认为没有必要的内容、增加上我自己补充的内容。
特别强调
-
react 由原来的17.0.2 升级为17.0.3 -
重点是
Three.js 由原来的0.125.2 升级为0.127.0 由于
Three.js 版本迭代,对于某些类的引用路径、方法和属性的使用 难免会有一些变化。 -
由于
Three.js 在0.126.0 版本时已经默认不包含.d.ts 文件,所以我们还需要额外单独安装 对应的typescript 类型定义文件包yarn add @types/three //npm i @types/three
特别强调
正文开始…
加载.obj( 模型) 文件
前言小絮
在之前所有的示例中,场景中的
让我们开始使用Blender
尽管我更喜欢
国外人很注意版权,所以很多教程中也都使用的是
假设你希望自己创建的模型或者项目要跟别人交流,而你使用破解版的
Blender 软件下载安装
当前版本为
2.92.0
下载地址:https://www.blender.org/download/
软件安装好之后,第一次启动
Blender 时会有一个弹窗,在弹窗中将界面语言由English 修改为 简体中文,这样软件界面就变成 简体中文了。
关于
我也是刚开始学习
我本身就会一些基础的
C4D 软件操作,所以在学习Blender 时并没有感觉特别吃力,只是觉得Blender 各种操作的快捷键实在是太多了,有点难记。如果你是第一次接触3D 建模软件,那…只能说,加油!
使用Blender 创建并导出3D 模型——.obj 文件
启动
关于
我们添加球体的操作是:物体模式下,点击 “添加
千万不要误操作成
因为
这句话是废话,要是没差别也不会是不同操作了。
所谓 融球 就是当
2 个融球彼此靠近一定程度后,2 个融球边缘会自动融合在一起,就是因为这个特性所以才被称为融球。融球本质上是Blender 一种即时计算的数据公式。而 球体 就是属于普通的网格,
2 个球体就算彼此靠得再近也不会发生相融的场景。
我们只是简单将这几个元素修改一下位置,不做任何属性的调整,然后就直接导出模型。
为什么不做复杂的调整?
答:因为我也刚学Blender ,还不太会,就先这样吧。
现在我们要导出刚才创建的
文件
在弹出的导出选项弹出中,我们什么也不修改,直接点击 导出
以下为默认的导出参数
在
Objects as 选项中:
OBJ 物体( 默认已勾选) OBJ 组( 默认未被勾选) - 材质组
( 默认未被勾选) - 动画
( 默认未被勾选) 在 变换 选项中:
- 缩放:
1.00 ( 默认值) - 路径模式:自动
( 默认值) - 前进:
-Z 前进( 默认值) - 向上:
Y 向上( 默认值) 在 集合数据 选项中:
- 应用修改器
( 默认已勾选) - 平滑组
( 默认未被勾选) Bitflag 平滑组( 默认未被勾选) - 写入法线
( 默认已勾选) - 包括
UV ( 默认已勾选) - 写入材质
( 默认已勾选) - 三角面
( 默认未被勾选) Curves as NURBS ( 默认未被勾选) - 多边形组
( 默认未被勾选) - 保持顶点顺序
( 默认未被勾选)
当导出完成后,我们会看到实际上产生了
- hello.obj
- hello.mtl
特别说明一下:
-
.obj 这个文件是用来储存3D 模型 数据的请注意这里面只包含
3D 模型 的数据和模型位置信息,并不包含Blender 场景中的镜头和灯光 -
.mtl 这个文件是用来储存 材质 数据的我们在
Blender 中创建的几个物体元素,实际上Blender 已经给他们添加了默认的一个可反光材质。由于是可反光材质,所以请记得一定要在
Three.js 场景中添加灯光,否则这些物体都会看不见的。在
Blender 中材质包含有 纹理贴图,尽管此刻我们并未给材质设置任何纹理贴图。
我们将得到的
接下来我们要开始编写
加载.obj 模型文件对应的类( 加载器) 为OBJLoader
最常用的方法为
.load()
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"
...
const loader = new OBJLoader()
loader.load(require('@/assets/model/hello.obj').default, (group) => {
console.log(group)
scene.add(group)
}, (event) => {
console.log(Math.floor((event.loaded * 100) / event.total) + '% loaded')
}, (error) => {
console.log(error.type)
})
也可以使用异步的
const promise = loader.loadAsync(require('@/assets/model/hello.obj').default, (event) => {
console.log(Math.floor((event.loaded * 100) / event.total) + '% loaded')
})
promise.then((group) => {
scene.add(group)
})
关于
如果你查看
即使你查看
OBJLoader 的源码,也会发现根本没有loadAsync() 方法,那……
这其实是因为
像别的编程语言文档,可能都会在某个类的介绍文章中,列出所继承父类的属性和方法,但是在
Three.js 中并未列出。
我向官方提交了一个建议:https://github.com/mrdoob/three.js/issues/21640
得到官方的回复内容为:
Indeed. And that's the reasons why I not vote to do this. This approach would require a lot of manual effort and thus is error prone when things change. The documentation pages contain references like: See the base Material class for common properties. Same for methods. Considering that the documentation is not generated, I think it's better to stick with this approach.
官方回复的意思是:由于现在
Three.js 文档并不是自动生成的,如果那样做当发生变更时,每次都会产生大量的工作内容,所以…就先保持现状吧。
完整的示例代码如下:
src/components/hello-objloader/index.tsx
import { useRef, useEffect } from "react"
import * as Three from 'three'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"
import './index.scss'
const HelloOBJLoader = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
useEffect(() => {
if (canvasRef.current === null) { return }
const renderer = new Three.WebGLRenderer({ canvas: canvasRef.current })
const scene = new Three.Scene()
const camera = new Three.PerspectiveCamera(45, 2, 0.1, 100)
camera.position.set(10, 0, 10)
const light = new Three.HemisphereLight(0xFFFFFF, 0x333333, 1)
scene.add(light)
const control = new OrbitControls(camera, canvasRef.current)
control.update()
const loader = new OBJLoader()
loader.load(require('@/assets/model/hello.obj').default, (group) => {
console.log(group)
scene.add(group)
}, (event) => {
console.log(Math.floor((event.loaded * 100) / event.total) + '% loaded')
}, (error) => {
console.log(error.type)
})
const render = () => {
renderer.render(scene, camera)
window.requestAnimationFrame(render)
}
window.requestAnimationFrame(render)
const handleResize = () => {
if (canvasRef.current === null) { return }
const width = canvasRef.current.clientWidth
const height = canvasRef.current.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height, false)
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return (
<canvas ref={canvasRef} className='full-screen' />
)
}
export default HelloOBJLoader
切记一定要给场景添加灯光,否则即使加载模型成功了,场景漆黑一片,也啥也看不见。
此刻,我们在
加载.mtl( 材质和纹理) 文件
首先,我们需要在
在Blender 中给物体设置材质,添加纹理
具体步骤为:
-
先准备一张 金属纹理贴图,将该图片文件命名为
metal_texture.jpg ,暂且存放在hello.blend 同级目录下。随着项目复杂,纹理图片资源变多,更加合理的是创建一个
texture 的目录,用来专门存放纹理图片资源 -
在不选择任何物体的前提下,在
Blender 右侧选项面板中,找到 “材质属性” 面板,我们会看到 默认材质Material ,在 “表( 曲) 面- 基础色” 设置中,找到 “基础色”,点击左侧的 小圆点,在弹出菜单中选择 “图像纹理”。如果你不小心点击的是 “基础色” 右侧的区域,则会出现 调色板
-
此时 基础色 已修改为 图像纹理 状态,我们点击 “打开”,在弹窗中找到
metal_texture.jpg 并点击 “打开图片" ,这样该纹理图片已添加到 材质Material 中了。 -
接下来我们需要做的就是依次选中
( 或全选) 立方体、圆柱体、椎体、球体,然后在材质属性面板中,找到 “材质图标”,鼠标放上去会显示提示文字:浏览要关联的材质,点击该图标,选择Material 请注意是一个图标,而不是什么按钮,更不要点击 “新建”
-
至此,我们已经将所有物体均设置一个相同材质纹理图片。
如果感兴趣,你完全可以自己给不同物体设置不同的材质纹理图片
让我们先简单渲染一下,看看效果吧。
首先我们调整好场景视角,然后执行 “视图Ctrl + Alt + Numpad0
。
然后我们点击
假设你不会
Blender ,也没有关系,可以忽略我上面这段关于如何在Blender 中创建模型和添加材质、纹理的操作。
导出模型
“文件
我们看一下
材质模版库(MTL)或
实际看看
我们可以使用 记事本 或
# Blender MTL File: 'hello.blend'
# Material Count: 1
newmtl Material
Ns 323.999994
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
map_Kd metal_texture.jpg
我们可以看到最后一行 map_Kd metal_texture.jpg
,大概也可以猜到这一行表明材质纹理图片的路径和文件名,并且路径是相对路径。
由于我们将
metal_texture.jpg 存放在hello.blend 同一目录下。
关于
下面我将针对上面的
.mtl 文件内容进行逐一解释
- #:注释内容
- newtml:材质的名称
- Ns:反射指数,即反射高光度,值越高则高光越密集,一般取值范围
0 - 1000 - Ka:材质的环境光,一般取值范围
0 - 1 - Kd:散射光
- Ks:镜面光
- Ni:折射光,一般取值范围
0.01 - 10 - d:渐隐指数,取值范围为
0 - 1 ,当值为1 时完全不透明,当值为0 时完全透明 - illum:材质的光照模型,值为整数,取值范围为
0 - 10 - map_Kd:漫反射指定颜色纹理贴图文件路径
上面属性中
取值 | 光照模型 | 中文名 |
---|---|---|
0 | Color on and Ambient off | 色彩开,阴影色关 |
1 | Color on and Ambient on | 色彩开,阴影色开 |
2 | Highlight on | 高光开 |
3 | Reflection on and Ray trace on | 反射开,光线追踪开 |
4 | Transparency: Glass on / Reflection: Ray trace on | 透明: 玻璃开 反射:光线追踪开 |
5 | Reflection: Fresnel on and Ray trace on | 反射:菲涅尔衍射开,光线追踪开 |
6 | Transparency: Refraction on / Reflection: Fresnel off and Ray trace on | 透明:折射开 反射:菲涅尔衍射关,光线追踪开 |
7 | Transparency: Refraction on / Reflection: Fresnel on and Ray trace on | 透明:折射开 反射:菲涅尔衍射开,光线追踪开 |
8 | Reflection on and Ray trace off | 反射开,光线追踪关 |
9 | Transparency: Glass on / Reflection: Ray trace off | 透明: 玻璃开 反射:光线追踪关 |
10 | Casts shadows onto invisible surfaces | 投射阴影于不可见表面 |
那么问题来了,由于
最简单的解决办法,就是将
public 目录里的文件是不会被webpack 编译重命名的。
拷贝文件到
我们在
至此,前期准备工作完毕,接下来转到
加载.mtl 表面着色器( 材质) 对应的类( 加载器) 为MTLLoader
在
Three.js 中,各种加载器的用法几乎完全相同。
我们需要做的事情就是:
-
先使用
MTLLoader 实例 加载材质( 纹理图片) 资源特别强调一下,
MTLLoader 实例 加载得到的对象类型并不是Three.Metrial ,而是MTLLoader.MaterialCreator -
加载完成后,将得到的
MaterialCreator 实例 赋予给OBJLoader 实例这样
OBJLoader 以后加载的任意模型都会自动应用该MaterialCreator 材质 -
然后再让
OBJLoader 实例 去加载模型加载完成后,将模型添加到场景中
实际代码:
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader"
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"
...
const mtlLoader = new MTLLoader()
const objLoader = new OBJLoader()
mtlLoader.load('./model/hello.mtl', (materialCreator) => {
objLoader.setMaterials(materialCreator)
objLoader.load('./model/hello.obj', (group) => {
scene.add(group)
})
})
请注意上述代码加载的资源地址为 “./model/hello.mtl” 和 “./model/hello.obj”,这里是相对路径,相对编译之后的
index.html 而言。
完整的代码:
import { useRef, useEffect } from "react"
import * as Three from 'three'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader"
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"
import './index.scss'
const HelloOBJLoader = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
useEffect(() => {
if (canvasRef.current === null) { return }
const renderer = new Three.WebGLRenderer({ canvas: canvasRef.current })
const scene = new Three.Scene()
const camera = new Three.PerspectiveCamera(45, 2, 0.1, 100)
camera.position.set(10, 0, 10)
const light = new Three.HemisphereLight(0xFFFFFF, 0x333333, 1)
scene.add(light)
const mtlLoader = new MTLLoader()
const objLoader = new OBJLoader()
mtlLoader.load('./model/hello.mtl', (materialCreator) => {
objLoader.setMaterials(materialCreator)
objLoader.load('./model/hello.obj', (group) => {
scene.add(group)
})
})
const control = new OrbitControls(camera, canvasRef.current)
control.update()
const render = () => {
renderer.render(scene, camera)
window.requestAnimationFrame(render)
}
window.requestAnimationFrame(render)
const handleResize = () => {
if (canvasRef.current === null) { return }
const width = canvasRef.current.clientWidth
const height = canvasRef.current.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height, false)
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return (
<canvas ref={canvasRef} className='full-screen' />
)
}
export default HelloOBJLoader
至此,我们已经讲解完如何加载
.obj 文件类型的一些特别之处
本文讲解的是在
这里要针对性的进行补充:
-
Blender 可以导出N 多种文件格式.obj 仅仅是其中一种 -
Three.js 支持多种3D 文件模型格式本文讲解的是加载
.obj
-
行业内比较广泛使用
-
.obj 是一种纯文本格式本文中我们使用记事本打开了
.mtl 文件,并未打开.obj 文件,感兴趣的可以自己尝试看看具体都是什么内容。 -
.obj 包含的内容有:Mesh( 网格) 、按组/ 物体分离、材质/ 纹理、NURBS 曲线和曲面 -
.obj 不包含( 不支持导出) 的内容有:网格顶点颜色、骨架、动画、灯光、相机、空物体、父子关系或变换
以上特性中不难看出
优点:只包含物体模型数据本身
缺点:不包含其他复杂元素
接下来,我们将在下一章节中,讲解另外一种
为什么要自己学习Blender ?
我个人认为,如果你想深入学习
你可以不精通,但是基础的操作你要掌握,这样非常利于你对于
如果你不会
你总不能完全依靠别人给你提供建好的模型,做到饭来张口的状态吧。
自己能够掌握
这也是为什么本文花了大量篇幅在一步一步讲解
我也是
Blender 新手小白,希望我们一起学习,一起加油。
如果本文中讲解的
这里提供
https://threejsfundamentals.org/threejs/resources/models/windmill_2/windmill-fixed.mtl
https://threejsfundamentals.org/threejs/resources/models/windmill_2/windmill.obj
我们下一章节见。