Threejs实现mtl模型渲染
需求:mtl模型渲染、设备结构润滑
版本:"three": "^0.168.0",
框架选型:vue3+vite
效果展示:
设备正常状态:
部分结构润滑:
threejs库安装:
pnpm add three@0.168.0
这边借鉴了Threejs示例:
导入相关loader:
import * as THREE from "three";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
创建场景:
let scene = new THREE.Scene();
初始化相机及位置调整:
let camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
20
);
// 相机在z轴的位置
camera.position.z = 2.5;
// 将相加添加到场景中
scene.add(camera);
光源设置(环境光or平行光根据需求而定):
/**
* AmbientLight 环境光
* color - (可选)一个表示颜色的 color的 实例、字符串或数字,默认为一个白色的color对象
* intensity -(可选)光照的强度,默认值为1
*
*/
const ambientLight = new THREE.AmbientLight("#fff", 0.2);
// 将环境光加入到场景
scene.add(ambientLight);
/**
* PointLight 平行光
* color - (可选)一个表示颜色的 color的 实例、字符串或数字,默认为一个白色的color对象
* intensity -(可选)光照的强度,默认值为1
*
*/
const pointLight = new THREE.PointLight("#fff", 100);
// 在相机中加入平行光
camera.add(pointLight);
将相机放入到场景中:
// 将相加添加到场景中
scene.add(camera);
模型定义与读取(注意:模型文件一定要放在根目录public下):
// 定义模型
let obj;
// 设置模型 如果是润滑状态 读取透明模型
if (rhFlag.value) {
obj = {
mtl: "/static/male02/arm_op.mtl",
obj: "/static/male02/arm_op.obj",
};
} else {
obj = { mtl: "/static/male02/arm.mtl", obj: "/static/male02/arm.obj" };
}
// 读取 mtl 和obj文件
await load_mtl_obj_Fn(obj);
这里有两套(分别为常规模型和润滑中的模型.mtl和.obj):
这里需要注意一下:
如果我们拿到UI给的模型文件进行下载后mtl文件和obj文件中的图片地址默认会使用绝对路径,
我们需要手动修改;
继续下一步:
模型加载:
// 加载模型 单独定义
const load_mtl_obj_Fn = (models) => {
return new Promise((resolve, reject) => {
// 进度监控的方法
const onProgress = function (xhr) {
if (xhr.lengthComputable) {
const percentComplete = (xhr.loaded / xhr.total) * 100;
console.log(percentComplete.toFixed(2) + "% downloaded");
}
};
// 加载 mtl
new MTLLoader().load(models.mtl, function (materials) {
// 载入纹理设置
materials.preload();
// 加载 obj 模型 并且将纹理贴上去
new OBJLoader().setMaterials(materials).load(
models.obj,
function (object) {
// 设置模型 y 轴位置
object.position.y = -0.75;
// 设置模型缩放 初始化的
object.scale.setScalar(0.00005);
// 将模型 放到场景中
scene.add(object);
resolve();
},
onProgress
);
});
});
};
渲染器设置:
/** WebGLRenderer 的参数
* parameters - (可选) 该对象的属性定义了渲染器的行为。也可以完全不传参数。在所有情况下,当 缺少参数时,它将采用合理的默认值。 以下是合法参数:
canvas - 一个供渲染器绘制其输出的canvas 它和下面的domElement属性对应。 如果没有传这个参数,会创建一个新canvas
context - 可用于将渲染器附加到已有的渲染环境(RenderingContext)中。默认值是null
precision - 着色器精度. 可以是 "highp", "mediump" 或者 "lowp". 如果设备支持,默认为"highp" .
alpha - controls the default clear alpha value. When set to true, the value is 0. Otherwise it's 1. Default is false.
premultipliedAlpha - renderer是否假设颜色有 premultiplied alpha. 默认为true
antialias - 是否执行抗锯齿。默认为false.
stencil - 绘图缓存是否有一个至少8位的模板缓存(stencil buffer)。默认为true
preserveDrawingBuffer -是否保留缓直到手动清除或被覆盖。 默认false.
powerPreference - 提示用户代理怎样的配置更适用于当前WebGL环境。 可能是"high-performance", "low-power" 或 "default"。默认是"default". 详见WebGL spec
failIfMajorPerformanceCaveat - 检测渲染器是否会因性能过差而创建失败。默认为false。详见 WebGL spec for details.
depth - 绘图缓存是否有一个至少6位的深度缓存(depth buffer )。 默认是true.
logarithmicDepthBuffer - 是否使用对数深度缓存。如果要在单个场景中处理巨大的比例差异,就有必要使用。 Note that this setting uses gl_FragDepth if available which disables the Early Fragment Test optimization and can cause a decrease in performance. 默认是false。 示例:camera / logarithmicdepthbuffer
*/
let renderer = new THREE.WebGLRenderer({ antialias: true });
// 设置设备像素比,通过用于避免 HiDPI 设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
// .setSize( width: integer, height: integer ,updateStyle:boolean):undefined
// 将输出canvas的大小调整为(width,height)并考虑设备像素比,且将视口从(0,0) 开始调整到适合大小 将updateStyle 设置为false
// 以阻止对 canvas 的样式做任何改变
renderer.setSize(window.innerWidth, window.innerHeight);
// .setAnimationLoop (callback: function) :undefined
// callback - 每个可用帧都会调用的函数, 如果传入 null 所有正在运行的动画都会停止
// 可用来代替 requestAnimationFrame的内置函数,对于webXR项目,必须使用此函数
renderer.setAnimationLoop(animate);
// 将渲染器的结果放到 div 盒子中
container.value.appendChild(renderer.domElement);
实例化模型控制类:
/**
* OrbitControls 参数如下
* camera - (必选)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身
* domElement - (可选)用于事件监听的HTML 元素
*/
const controls = new OrbitControls(camera, renderer.domElement);
// 你能够将相机向内移动多少 默认值为0 (仅适用于PerspectiveCamera)
controls.minDistance = 0;
// 你能够将相机向外移动多少 默认值为infinite (仅适用于PerspectiveCamera)
controls.maxDistance = 10;
至此模型就可以出来了。
接下来进行 模型设备润滑:
async function startRh() {
const isChecked = deviceList.some((device) => device.checked);
if (isChecked) {
// 将盒子清空
container.value.innerHTML = "";
// 设置润滑开启状态
rhFlag.value = !rhFlag.value;
// 初始化模型
await init();
// 引入图片纹理
const dc = [];
dc[0] = new THREE.TextureLoader().load("/static/tsg/dc_green.jpg");
dc[1] = new THREE.TextureLoader().load("/static/tsg/dc_red.jpg");
// 分频器
const fpq = [];
fpq[0] = new THREE.TextureLoader().load("/static/tsg/fpq_bai.jpg");
fpq[1] = new THREE.TextureLoader().load("/static/tsg/fpq_green.jpg");
fpq[2] = new THREE.TextureLoader().load("/static/tsg/fpq_red.jpg");
fpq[3] = new THREE.TextureLoader().load("/static/tsg/fpq_yellow.jpg");
// 线
const line = [];
line[0] = new THREE.TextureLoader().load("/static/tsg/1.jpg");
line[1] = new THREE.TextureLoader().load("/static/tsg/2.jpg");
line[2] = new THREE.TextureLoader().load("/static/tsg/3.jpg");
line[3] = new THREE.TextureLoader().load("/static/tsg/4.jpg");
line[4] = new THREE.TextureLoader().load("/static/tsg/5.jpg");
// 获取的模型纹理的数组
scene.children[2].children.forEach((item, index) => {
// 查找当前机构是否是 选中的机构
if (findDevice(item.name)) {
if (item.name.includes("dc")) {
item.material.map = dc[0];
let i = 1;
// 动态切花 选中状态
setInterval(() => {
item.material.map = dc[i];
i++;
if (i > 1) {
i = 0;
}
}, 1000);
} else if (item.name.includes("fpq")) {
item.material.map = fpq[0];
let i = 1;
setInterval(() => {
item.material.map = fpq[i];
i++;
if (i > 3) {
i = 0;
}
}, 1000);
} else if (item.name.includes("line")) {
item.material.map = line[0];
let i = 1;
setInterval(() => {
item.material.map = line[i];
// 设置横向 与垂直的重复
item.material.wrapS = THREE.RepeatWrapping;
item.material.wrapT = THREE.RepeatWrapping;
// 设置重复次数
item.material.map.repeat.set(4, 4);
i++;
if (i > 4) {
i = 0;
}
}, 1000);
}
}
});
if (!rhFlag.value) {
deviceList.forEach((item) => (item.checked = false));
}
} else {
alert("请至少选择一个设备");
}
}
// 查找当前机构是否被选中了
const findDevice = (name) => {
const foundDevice = deviceList.find((device) => name.includes(device.name));
return foundDevice ? foundDevice.checked : false;
};
效果:
源码:
<template>
<Layout>
<template #content>
<div class="container" ref="container"></div>
</template>
</Layout>
<div class="mangers">
<el-checkbox-group
v-model="checkedCities"
@change="handleCheckedCitiesChange"
>
<el-checkbox
v-for="city in deviceList"
:key="city.name + city.nickname"
:label="city.nickname"
:value="city"
@click="city.checked = !city.checked"
style="color: aliceblue; margin: 5px 0"
border
>
{{ city.nickname }}
</el-checkbox>
</el-checkbox-group>
<el-button @click="startRh" type="primary">开始</el-button>
</div>
</template>
<script setup >
import Layout from "@/layout/layout.vue";
import * as THREE from "three";
import { onMounted, reactive, ref } from "vue";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import shopCar from "./components/shopCar.vue";
const checkAll = ref(false);
const isIndeterminate = ref(true);
const checkedCities = ref([]);
const handleCheckAllChange = (val) => {
checkedCities.value = val ? deviceList : [];
isIndeterminate.value = false;
};
const handleCheckedCitiesChange = (value) => {
const checkedCount = value.length;
checkAll.value = checkedCount === deviceList.length;
isIndeterminate.value = checkedCount > 0 && checkedCount < deviceList.length;
};
const container = ref(null);
// 定义所有机构的数组
const deviceList = reactive([
{
name: "xiangbiliangtoubu",
nickname: "后背轮",
checked: false,
},
{
name: "houbeilunhualun",
nickname: "后备轮滑轮",
checked: false,
},
{
name: "lizhujigou",
nickname: "立柱机构",
checked: false,
},
{
name: "bianfujigou",
nickname: "变幅机构",
checked: false,
},
{
name: "yalunzhou",
nickname: "压轮轴",
checked: false,
},
{
name: "xuanzhuanjigou1",
nickname: "旋转机构1",
checked: false,
},
{
name: "dabigen",
nickname: "大臂根",
checked: false,
},
{
name: "xuanzhuanjigou2",
nickname: "旋转机构2",
checked: false,
},
{
name: "xuanzhuanjigou3",
nickname: "旋转机构3",
checked: false,
},
{
name: "houbeilunyalun",
nickname: "后背轮压轮",
checked: false,
},
{
name: "xuanzhuanjigou4",
nickname: "旋转机构4",
checked: false,
},
]);
// 定义润滑状态
const rhFlag = ref(false);
// 定义相机 场景 渲染器
let camera, scene, renderer;
onMounted(() => {
init();
});
// 初始化 模型
const init = async () => {
return new Promise(async (resolve, reject) => {
// 初始化 相机位置
/* PerspectiveCamera 有四个参数 如下
* fov - 摄像机视锥体垂直视野角度
aspect - 摄像机视锥体长宽比
near - 摄像机视锥体近端面
far - 摄像机视锥体远端面
*/
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
20
);
// 相机在z轴的位置
camera.position.z = 2.5;
// 初始化场景
scene = new THREE.Scene();
// 读取 主题背景色
const el = document.documentElement;
const background =
getComputedStyle(el).getPropertyValue("--backgroundTheme");
// 设置场景背景色
scene.background = new THREE.Color("black");
// 设置光源 环境光
/**
* AmbientLight 环境光
* color - (可选)一个表示颜色的 color的 实例、字符串或数字,默认为一个白色的color对象
* intensity -(可选)光照的强度,默认值为1
*
*/
const ambientLight = new THREE.AmbientLight("#fff", 0.2);
// 将环境光加入到场景
scene.add(ambientLight);
// 设置平行光
/**
* PointLight 平行光
* color - (可选)一个表示颜色的 color的 实例、字符串或数字,默认为一个白色的color对象
* intensity -(可选)光照的强度,默认值为1
*
*/
const pointLight = new THREE.PointLight("#fff", 100);
// 在相机中加入平行光
camera.add(pointLight);
// 将相加添加到场景中
scene.add(camera);
// 定义模型
let obj;
// 设置模型 如果是润滑状态 读取透明模型
if (rhFlag.value) {
obj = {
mtl: "/static/male02/arm_op.mtl",
obj: "/static/male02/arm_op.obj",
};
} else {
obj = { mtl: "/static/male02/arm.mtl", obj: "/static/male02/arm.obj" };
}
// 读取 mtl 和obj文件
await load_mtl_obj_Fn(obj);
// 设置渲染器
/** WebGLRenderer 的参数
* parameters - (可选) 该对象的属性定义了渲染器的行为。也可以完全不传参数。在所有情况下,当缺少参数时,它将采用合理的默认值。 以下是合法参数:
canvas - 一个供渲染器绘制其输出的canvas 它和下面的domElement属性对应。 如果没有传这个参数,会创建一个新canvas
context - 可用于将渲染器附加到已有的渲染环境(RenderingContext)中。默认值是null
precision - 着色器精度. 可以是 "highp", "mediump" 或者 "lowp". 如果设备支持,默认为"highp" .
alpha - controls the default clear alpha value. When set to true, the value is 0. Otherwise it's 1. Default is false.
premultipliedAlpha - renderer是否假设颜色有 premultiplied alpha. 默认为true
antialias - 是否执行抗锯齿。默认为false.
stencil - 绘图缓存是否有一个至少8位的模板缓存(stencil buffer)。默认为true
preserveDrawingBuffer -是否保留缓直到手动清除或被覆盖。 默认false.
powerPreference - 提示用户代理怎样的配置更适用于当前WebGL环境。 可能是"high-performance", "low-power" 或 "default"。默认是"default". 详见WebGL spec
failIfMajorPerformanceCaveat - 检测渲染器是否会因性能过差而创建失败。默认为false。详见 WebGL spec for details.
depth - 绘图缓存是否有一个至少6位的深度缓存(depth buffer )。 默认是true.
logarithmicDepthBuffer - 是否使用对数深度缓存。如果要在单个场景中处理巨大的比例差异,就有必要使用。 Note that this setting uses gl_FragDepth if available which disables the Early Fragment Test optimization and can cause a decrease in performance. 默认是false。 示例:camera / logarithmicdepthbuffer
*/
renderer = new THREE.WebGLRenderer({ antialias: true });
// 设置设备像素比,通过用于避免 HiDPI 设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
// .setSize( width: integer, height: integer ,updateStyle:boolean):undefined
// 将输出canvas的大小调整为(width,height)并考虑设备像素比,且将视口从(0,0) 开始调整到适合大小 将updateStyle 设置为false
// 以阻止对 canvas 的样式做任何改变
renderer.setSize(window.innerWidth, window.innerHeight);
// .setAnimationLoop (callback: function) :undefined
// callback - 每个可用帧都会调用的函数, 如果传入 null 所有正在运行的动画都会停止
// 可用来代替 requestAnimationFrame的内置函数,对于webXR项目,必须使用此函数
renderer.setAnimationLoop(animate);
// 将渲染器的结果放到 div 盒子中
container.value.appendChild(renderer.domElement);
// 实例化 模型控制类
/**
* OrbitControls 参数如下
* camera - (必选)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身
* domElement - (可选)用于事件监听的HTML 元素
*/
const controls = new OrbitControls(camera, renderer.domElement);
// 你能够将相机向内移动多少 默认值为0 (仅适用于PerspectiveCamera)
controls.minDistance = 0;
// 你能够将相机向外移动多少 默认值为infinite (仅适用于PerspectiveCamera)
controls.maxDistance = 10;
// 监听页面窗口大小 配置 模型尺寸
window.addEventListener("resize", onWindowResize);
resolve();
});
};
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
// 这是更新投影变换矩阵
camera.updateProjectionMatrix();
// 重新设置场景
renderer.setSize(window.innerWidth, window.innerHeight);
}
//
function animate() {
// 设置动画效果
renderer.render(scene, camera);
}
// 加载模型 单独定义
const load_mtl_obj_Fn = (models) => {
return new Promise((resolve, reject) => {
// 进度监控的方法
const onProgress = function (xhr) {
if (xhr.lengthComputable) {
const percentComplete = (xhr.loaded / xhr.total) * 100;
console.log(percentComplete.toFixed(2) + "% downloaded");
}
};
// 加载 mtl
new MTLLoader().load(models.mtl, function (materials) {
// 载入纹理设置
materials.preload();
// 加载 obj 模型 并且将纹理贴上去
new OBJLoader().setMaterials(materials).load(
models.obj,
function (object) {
// 设置模型 y 轴位置
object.position.y = -0.75;
// 设置模型缩放 初始化的
object.scale.setScalar(0.00005);
// 将模型 放到场景中
scene.add(object);
resolve();
},
onProgress
);
});
});
};
async function startRh() {
const isChecked = deviceList.some((device) => device.checked);
if (isChecked) {
// 将盒子清空
container.value.innerHTML = "";
// 设置润滑开启状态
rhFlag.value = !rhFlag.value;
// 初始化模型
await init();
// 引入图片纹理
const dc = [];
dc[0] = new THREE.TextureLoader().load("/static/tsg/dc_green.jpg");
dc[1] = new THREE.TextureLoader().load("/static/tsg/dc_red.jpg");
// 分频器
const fpq = [];
fpq[0] = new THREE.TextureLoader().load("/static/tsg/fpq_bai.jpg");
fpq[1] = new THREE.TextureLoader().load("/static/tsg/fpq_green.jpg");
fpq[2] = new THREE.TextureLoader().load("/static/tsg/fpq_red.jpg");
fpq[3] = new THREE.TextureLoader().load("/static/tsg/fpq_yellow.jpg");
// 线
const line = [];
line[0] = new THREE.TextureLoader().load("/static/tsg/1.jpg");
line[1] = new THREE.TextureLoader().load("/static/tsg/2.jpg");
line[2] = new THREE.TextureLoader().load("/static/tsg/3.jpg");
line[3] = new THREE.TextureLoader().load("/static/tsg/4.jpg");
line[4] = new THREE.TextureLoader().load("/static/tsg/5.jpg");
// 获取的模型纹理的数组
scene.children[2].children.forEach((item, index) => {
// 查找当前机构是否是 选中的机构
if (findDevice(item.name)) {
if (item.name.includes("dc")) {
item.material.map = dc[0];
let i = 1;
// 动态切花 选中状态
setInterval(() => {
item.material.map = dc[i];
i++;
if (i > 1) {
i = 0;
}
}, 1000);
} else if (item.name.includes("fpq")) {
item.material.map = fpq[0];
let i = 1;
setInterval(() => {
item.material.map = fpq[i];
i++;
if (i > 3) {
i = 0;
}
}, 1000);
} else if (item.name.includes("line")) {
item.material.map = line[0];
let i = 1;
setInterval(() => {
item.material.map = line[i];
// 设置横向 与垂直的重复
item.material.wrapS = THREE.RepeatWrapping;
item.material.wrapT = THREE.RepeatWrapping;
// 设置重复次数
item.material.map.repeat.set(4, 4);
i++;
if (i > 4) {
i = 0;
}
}, 1000);
}
}
});
if (!rhFlag.value) {
deviceList.forEach((item) => (item.checked = false));
}
} else {
alert("请至少选择一个设备");
}
}
// 查找当前机构是否被选中了
const findDevice = (name) => {
const foundDevice = deviceList.find((device) => name.includes(device.name));
return foundDevice ? foundDevice.checked : false;
};
</script>
<style lang="less" scoped>
.container {
width: 100%;
height: 100%;
}
.btnBox {
width: 2.083333rem /* 400/192 */ /* 200/192 */;
height: 0.520833rem /* 100/192 */ /* 50/192 */;
position: fixed;
left: 0;
top: 0;
display: flex;
// background: red;
align-items: center;
button {
// width: 0.520833rem /* 100/192 */;
height: 0.260417rem /* 50/192 */;
}
}
.mangers {
width: 0.78125rem /* 150/192 */;
position: fixed;
z-index: 9999;
bottom: 0.9375rem /* 180/192 */ /* 120/192 */ /* 30/192 */ /* 5/192 */;
right: 30% /* 10/192 */;
// width: 40%;
// background: red;
}
.run {
position: fixed;
z-index: 9999;
bottom: 0.625rem /* 120/192 */ /* 30/192 */ /* 5/192 */;
right: 30% /* 250/192 */;
// width: 40%;
// background: red;
div {
width: 0.260417rem /* 50/192 */ /* 100/192 */;
padding: 0.1rem 0.2rem;
border-radius: 0.052083rem /* 10/192 */;
cursor: pointer;
color: #fff;
&:hover {
background: green;
color: white;
}
}
}
.divice {
width: 1.041667rem /* 200/192 */;
display: flex;
height: 30vh /* 300/192 */;
overflow: scroll;
flex-wrap: wrap;
box-shadow: inset 0 -5px 15px #ccc, inset 10px 0 15px #ccc;
div {
margin: 0.1rem;
padding: 0.1rem 0.2rem;
border-radius: 0.052083rem /* 10/192 */;
cursor: pointer;
color: #fff;
&:hover {
background: green;
color: white;
}
}
}
.deviceMR {
background: #409eff;
}
.deviceXZ {
background: green;
}
</style>
原文地址:https://blog.csdn.net/Alone_Endeavor/article/details/143001074
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!