碎碎念
很久没写记录了 其实中间接触了不少东西 docker啊 jenkins啊 fabric什么的 但是一直都懒得写 后面逐步写出来 当作个记录 不然有时候还要到处翻文档 蛮烦的 哪有自己写方便
效果
替换对应的节点 把不同的图片渲染到模型上去 至于 为什么模型是椅子 emmm 因为对应的模型公司还没做完 我只能找个模型 临时演示一下 如何从canvas上对应传递过去 涉及到另外一个组件 fabric 后面的文章会写这个
准备
首先得有个threejs吧 那就直接引吧
npm i three
然后既然是用ts 那就再引个@types
npm i @types/three
简单描述
公司的项目是服装订制 原理是使用glb模型上不同的node点 分别渲染不同的图片上去
threejs的原理或者更深的就不献丑了 只打算简单讲一下 我是怎么用的 给一个实际操作的例子能跑起来 不一定够优化 大家如果有更好的方案或者我有什么不合理的地方 劳烦指点一下
ps:属性和参数会直接引用官方文档的 然后给出相应链接 毕竟是0卡所以只给链接不会完整复制 那太水字数了
有个概念得清楚
就是一个demo是由哪些模块组成 简陋的写画个图
graph TD;
demo-->场景/scence;
demo-->相机/camera;
demo-->渲染器/renderer;
场景/scence-->空间;
场景/scence-->对象;
场景/scence-->光源;
相机/camera-->位置;
相机/camera-->方向;
相机/camera-->投影方式;
渲染器/renderer-->创建;
渲染器/renderer-->渲染;
渲染器/renderer-->属性;
笛卡尔坐标系的话 很重要但是不影响初步使用 借一个网图 出处不知道是哪 简单了解一下就好 想深入再仔细去研究
ps:百度上扣的 如有冒犯 联系我删掉
上手
html
<template>
<div :id="labelById"
style="width:100%;height:100%;"></div>
</template>
注意:id从父组件传入 不然多个canvas可能会出现 全在第一个canvas里面渲染 其他的里面是空的这种情况 尽量别名字全一样 如果只有一个模型 那你就写固定 无妨
引入THREE及所需
import * as THREE from "three" //全引 除非你很明确自己要什么
import { onMounted, watch, ref } from "vue"
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' //轨道控制器
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader" // 引入GLTFLoader加载器
import { DRACOLoader, } from "three/examples/jsm/loaders/DRACOLoader" //可以使用glTF管道将普通的glTF文件转换为Draco压缩的glTF文件
说明:我选择的加载模式是GLTFLoader 可以加载gltf以及glb格式的文件 还有其他格式自行选购
- gltf格式和glb格式的作用和不同(仅自己使用后的发现如有错误 劳烦大佬点拨):
-
gltf格式会有附带一个或多个?(我下的几个模型只一个 但是网上介绍可能多个)的bin文件 是用来存储几何数据(顶点、索引)动画数据 Skin 具体干嘛的 我不是很了解
-
glb格式的话这个文件的本身就包含bin文件的内容意味着上传的时候 只需要传一个就好
-
这两个文件格式的本体都会包含模型的一些数据是我们需要用到的参数 例如对应的node 例如一个模型正反两面 需要渲染不同的东西 就需要用到node 其他没怎么研究
-
OrbitControls轨道控制器的话 就像是 你想找个人给你拍照 他会拿着手机 换各种角度给你找个比较好的视角去拍 亦 或者像冬奥会的鹰眼系统 一个滑轨 无死角拍摄
-
至于这个DRACOLoader的话 有的模型 需要进行一定的压缩之类的操作 才能正常读取出来 毕竟模型不是我们来做 具体原因 我说不出个123 大家看着用
上面说到 一个demo 由场景 相机 以及渲染器 三个部分组成
那么 第一步
场景
const scene = new THREE.Scene()
没什么好说的仅创建场景 属性直接看官方文档
相机
我获取了document的宽高 这两个参数后面也会用到
const width = document.getElementById(props.labelById)!.clientWidth, height = document.getElementById(props.labelById)!.clientHeight,
camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000)
PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
- fov — 摄像机视锥体垂直视野角度(视野范围)
- aspect — 摄像机视锥体长宽比 (画布宽高比)
- near — 摄像机视锥体近端面 (近平面)
- far — 摄像机视锥体远端面 (远平面)
渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
WebGLRenderer( parameters : Object )
parameters - (可选) 该对象的属性定义了渲染器的行为。也可以完全不传参数。在所有情况下,当缺少参数时,它将采用合理的默认值。
至此 三个基本部分已经构成 下面对滑轨控制器、对象、光源等进行添加和配置
滑轨控制器
const controls = new OrbitControls(camera, renderer.domElement)
OrbitControls( object : Camera, domElement : HTMLDOMElement )
object: (必须)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。
domElement: 用于事件监听的HTML元素。
初始化属性
需要提前什么声明的也就上面的这些 然后就是初始化这些东西的属性了
const init = () => {
scene.background = new THREE.Color(0xf5f5f5) // 场景背景色
scene.fog = new THREE.FogExp2(0xf5f5f5, 0.002) // 线性雾
renderer.setPixelRatio(window.devicePixelRatio) // 设备像素比 Window.devicePixelRatio 可返回当前显示设备的物理像素分辨率与CSS像素分辨率之比
renderer.setSize(width, height) //画布大小
document.getElementById(props.labelById)?.appendChild(renderer.domElement) // 添加事件监听的HTML元素
camera.position.set(200, 200, 200) //相机位置
controls.listenToKeyEvents(window) //为指定的DOM元素添加按键监听 官方推荐将window作为指定的DOM元素。
controls.enableDamping = true //将其设置为true以启用阻尼(惯性),这将给控制器带来重量感。默认值为false。请注意,如果该值被启用,你将必须在你的动画循环里调用.update()。
controls.dampingFactor = 0.05 //当enableDamping设置为true的时候,阻尼惯性有多大。 Default is 0.05. 请注意,要使得这一值生效,你必须在你的动画循环里调用.update()。
controls.screenSpacePanning = false //定义当平移的时候摄像机的位置将如何移动。如果为true,摄像机将在屏幕空间内平移。 否则,摄像机将在与摄像机向上方向垂直的平面中平移
controls.minDistance = 100 //相机最小移动速度 默认为0
controls.maxDistance = 500 //相机最大移动速度 默认为Infinity
loadGLTF()//加载模型
const dirLight1 = new THREE.DirectionalLight(0xffffff) //平行光
dirLight1.position.set(1, 1, 1) //设置光的角度
scene.add(dirLight1)
const dirLight2 = new THREE.DirectionalLight(0x002288) //同上
dirLight2.position.set(- 1, - 1, - 1) //为了背面也有光效
scene.add(dirLight2)
const ambientLight = new THREE.AmbientLight(0x222222) //环境光
scene.add(ambientLight)
window.addEventListener('resize', onWindowResize) //监听窗口缩放
}
加载模型
const loadGLTF = () => {
try {
const loader = new GLTFLoader().setPath(props.threeDimensionalModelInfo.url) //加载模型 可以为空
if (props.dracoUrl) {
const dracoLoader = new DRACOLoader().setDecoderPath(props.dracoUrl) // 对特殊模型进行处理压缩
loader.setDRACOLoader(dracoLoader) //用于解码使用KHR_draco_mesh_compression扩展压缩过的文件
}
loader.load('', (gltf) => { //.load ( url : String, onLoad : Function, onProgress : Function, onError : Function )
let theModel = gltf.scene
theModel.traverse((o: any) => {
if (o.isMesh) {
o.castShadow = true //投射阴影
o.receiveShadow = true //接受阴影
}
})
theModel.rotation.y = Math.PI //旋转模型 0 ~ Math.PI
theModel.position.y = -1 //模型放置位置 按需填
theModel.scale.set(100, 100, 100) //比例 长宽高 看模型调试
threeDimensionalModelParent.value = theModel //存放一下原始模型 备用
scene.add(theModel) //把模型加入场景
props.threeDimensionalModelInfo.relevanceSwatch?.forEach((r: any) => {
if (r.texture && r.currentLocation)
selectSwatch(r) //对node节点进行操作 贴图或填充颜色
})
})
} catch (err) {
console.log(err)
message.error('模型加载失败')
}
}
GLTFLoader( manager : LoadingManager )
manager — 该加载器将要使用的 loadingManager 。默认为 THREE.DefaultLoadingManager。
创建一个新的GLTFLoader。
选择样板
const selectSwatch = async (e: any) => {
let newMtl
if (e.texture) {
let txt: any
if (e.texture instanceof String) { //数据可能是地址也可能直接是图片源数据
txt = new THREE.TextureLoader().load(e.texture)
} else if (e.texture instanceof ImageData) {
txt = new THREE.DataTexture(e.texture.data, e.texture.width, e.texture.height)
txt.needsUpdate = true //更新
}
txt.repeat.set(1, 1) //纹理在整个表面上重复多少次 巨坑点 哪怕不重复也会最后一像素无限拉长
newMtl = new THREE.MeshPhongMaterial({
map: txt,
shininess: 60
})
setMaterial(threeDimensionalModelParent.value, e.currentLocation, newMtl)
} else if (e.color) {
newMtl = new THREE.MeshPhongMaterial({
color: parseInt('0x' + e.color),
shininess: 60
})
setMaterial(threeDimensionalModelParent.value, e.currentLocation, newMtl)
}
}
MeshPhongMaterial( parameters : Object )
parameters - (可选)用于定义材质外观的对象,具有一个或多个属性。 材质的任何属性都可以从此处传入(包括从Material继承的任何属性)。
属性color例外,其可以作为十六进制字符串传递,默认情况下为 0xffffff(白色),内部调用Color.set(color)。
Texture( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, encoding )
贴图
const setMaterial = (parent: any, currentLocation: any = undefined, mtl: any = undefined) => {
let initMtl = new THREE.MeshPhongMaterial() //初始化
parent.traverse((o: any) => {
if (!props.threeDimensionalModelInfo.relevanceSwatch.find((r: any) => o.nameID == r.currentLocation)) {
o.nameID = null
o.material = initMtl
} //如果参数里没有对应的 就初始化
if (o.isMesh) {
if (o.name.includes(currentLocation)) {
o.nameID = currentLocation
o.material = mtl
}//有就渲染
}
})
}
监听窗口变化
const onWindowResize = () => {
let newWidth = document.getElementById(props.labelById)!.clientWidth, newHeight = document.getElementById(props.labelById)!.clientHeight
camera.aspect = newWidth / newHeight
camera.updateProjectionMatrix()
renderer.setSize(newWidth, newHeight)
}
渲染
const animate = () => {
requestAnimationFrame(animate)
controls.update()
render()
}
const render = () => {
renderer.render(scene, camera)
}
完整代码
<template>
<div :id="labelById"
style="width:100%;height:100%;"></div>
</template>
<script setup lang="ts">
import * as THREE from "three"
import { onMounted, watch, ref } from "vue"
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader" // 引入GLTFLoader加载器
import { DRACOLoader, } from "three/examples/jsm/loaders/DRACOLoader"
import { message } from 'ant-design-vue'
export interface ThreeDimensionalModelInfo {
relevanceSwatch: Array<RelevanceSwatch>
url: string
}
export interface RelevanceSwatch {
customImageId: number
color?: string
texture?: string
currentLocation: string
}
const props = defineProps<{
labelById: string
threeDimensionalModelInfo: ThreeDimensionalModelInfo
dracoUrl: string
}>()
watch(() => props.threeDimensionalModelInfo?.url, (state) => {
if (state)
load()
})
onMounted(() => {
if (props.threeDimensionalModelInfo?.url)
load()
})
const load = () => {
watch(() => props.threeDimensionalModelInfo, (state) => {
state.relevanceSwatch?.forEach((r: any) => {
selectSwatch(r)
})
if (!state.relevanceSwatch.length) {
setMaterial(threeDimensionalModelParent.value)
}
}, {
deep: true
})
const scene = new THREE.Scene()
const width = document.getElementById(props.labelById)!.clientWidth, height = document.getElementById(props.labelById)!.clientHeight,
camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000)
const renderer = new THREE.WebGLRenderer({ antialias: true })
const controls = new OrbitControls(camera, renderer.domElement)
const threeDimensionalModelParent = ref<any>({})
const init = () => {
scene.background = new THREE.Color(0xf5f5f5) // 场景背景色
scene.fog = new THREE.FogExp2(0xf5f5f5, 0.002) // 线性雾
renderer.setPixelRatio(window.devicePixelRatio) // 设备像素比 Window.devicePixelRatio 可返回当前显示设备的物理像素分辨率与CSS像素分辨率之比
renderer.setSize(width, height) //画布大小
document.getElementById(props.labelById)?.appendChild(renderer.domElement) // 添加事件监听的HTML元素
camera.position.set(200, 200, 200) //相机位置
controls.listenToKeyEvents(window) //为指定的DOM元素添加按键监听 官方推荐将window作为指定的DOM元素。
controls.enableDamping = true //将其设置为true以启用阻尼(惯性),这将给控制器带来重量感。默认值为false。请注意,如果该值被启用,你将必须在你的动画循环里调用.update()。
controls.dampingFactor = 0.05 //当enableDamping设置为true的时候,阻尼惯性有多大。 Default is 0.05. 请注意,要使得这一值生效,你必须在你的动画循环里调用.update()。
controls.screenSpacePanning = false //定义当平移的时候摄像机的位置将如何移动。如果为true,摄像机将在屏幕空间内平移。 否则,摄像机将在与摄像机向上方向垂直的平面中平移
controls.minDistance = 100 //相机最小移动速度 默认为0
controls.maxDistance = 500 //相机最大移动速度 默认为Infinity
loadGLTF()//加载模型
const dirLight1 = new THREE.DirectionalLight(0xffffff) //平行光
dirLight1.position.set(1, 1, 1) //设置光的角度
scene.add(dirLight1)
const dirLight2 = new THREE.DirectionalLight(0x002288) //同上
dirLight2.position.set(- 1, - 1, - 1) //为了背面也有光效
scene.add(dirLight2)
const ambientLight = new THREE.AmbientLight(0x222222) //环境光
scene.add(ambientLight)
window.addEventListener('resize', onWindowResize) //监听窗口缩放
}
const loadGLTF = () => {
try {
const loader = new GLTFLoader().setPath(props.threeDimensionalModelInfo.url) //加载模型 可以为空
if (props.dracoUrl) {
const dracoLoader = new DRACOLoader().setDecoderPath(props.dracoUrl) // 对特殊模型进行处理压缩
loader.setDRACOLoader(dracoLoader) //用于解码使用KHR_draco_mesh_compression扩展压缩过的文件
}
loader.load('', (gltf) => { //.load ( url : String, onLoad : Function, onProgress : Function, onError : Function )
let theModel = gltf.scene
theModel.traverse((o: any) => {
if (o.isMesh) {
o.castShadow = true //投射阴影
o.receiveShadow = true //接受阴影
}
})
theModel.rotation.y = Math.PI //旋转模型 0 ~ Math.PI
theModel.position.y = -1 //模型放置位置
theModel.scale.set(100, 100, 100) //比例 长宽高 看模型调试
threeDimensionalModelParent.value = theModel //存放一下原始模型 备用
scene.add(theModel) //把模型加入场景
props.threeDimensionalModelInfo.relevanceSwatch?.forEach((r: any) => {
if (r.texture && r.currentLocation)
selectSwatch(r) //对node节点进行操作 为了贴素材
})
})
} catch (err) {
console.log(err)
message.error('模型加载失败')
}
}
const selectSwatch = async (e: any) => {
let newMtl
if (e.texture) {
let txt: any
if (e.texture instanceof String) { //数据可能是地址也可能直接是图片源数据
txt = new THREE.TextureLoader().load(e.texture)
} else if (e.texture instanceof ImageData) {
txt = new THREE.DataTexture(e.texture.data, e.texture.width, e.texture.height)
txt.needsUpdate = true //更新
}
txt.repeat.set(1, 1) //平铺是否重复 x y轴重复数量
newMtl = new THREE.MeshPhongMaterial({
map: txt,
shininess: 60
})
setMaterial(threeDimensionalModelParent.value, e.currentLocation, newMtl)
} else if (e.color) {
newMtl = new THREE.MeshPhongMaterial({
color: parseInt('0x' + e.color),
shininess: 60
})
setMaterial(threeDimensionalModelParent.value, e.currentLocation, newMtl)
}
}
const setMaterial = (parent: any, currentLocation: any = undefined, mtl: any = undefined) => {
let initMtl = new THREE.MeshPhongMaterial()
parent.traverse((o: any) => {
if (!props.threeDimensionalModelInfo.relevanceSwatch.find((r: any) => o.nameID == r.currentLocation)) {
o.nameID = null
o.material = initMtl
}
if (o.isMesh) {
if (o.name.includes(currentLocation)) {
o.nameID = currentLocation
o.material = mtl
}
}
})
}
const onWindowResize = () => {
let newWidth = document.getElementById(props.labelById)!.clientWidth, newHeight = document.getElementById(props.labelById)!.clientHeight
camera.aspect = newWidth / newHeight // 摄像机视锥体长宽比
camera.updateProjectionMatrix() //更新摄像机投影矩阵。在任何参数被改变以后必须被调用。
renderer.setSize(newWidth, newHeight)
}
const animate = () => {
requestAnimationFrame(animate) //重绘
controls.update() //更新控制器
render()
}
const render = () => {
renderer.render(scene, camera)
}
init()
animate()
}
</script>
<style scoped></style>