自学内容网 自学内容网

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)!