3d模型快速生成
一、概述
本文讲述使用Ready Player Me、Blender、Mixamo、vue3快速构建web端3D模型的生成与展示。
效果:

涉及到的工具:
chatgpt或者其他AI大语言模型 https://chatgpt.com/
即梦
Ready Player Me
Blender
Mixamo
vue3
二、具体步骤

1. chatgpt生成文生图提示词,参考如下:
你是一位女性购车客户,名字叫林婉青,27岁,是杭州一所小学的语文老师。这是你人生中第一次买车,预算在20-30万元之间,购车目的是通勤代步和偶尔家庭出游。你倾向选择油耗低、空间大、保值率高的合资品牌,如丰田或本田,但也愿意了解奥迪车型。你目前正在咨询杭州地区某一汽奥迪4S店的销售员。请自然代入角色,与销售进行真实对话。请注意以下几点:1.你的回答会用于语音合成,故你的回答中不要出现特殊字符,例如 -
用 至 代替;2.你要模拟的是一个正常人,你可以拥有喜怒哀乐等七情六欲;3.回答需要口语化;4.不要出现叠词。
上述是一个角色扮演AI的提示词,请根据这份提示词生成用于文生图(生成证件照)的提示词,要中文
2. 即梦生成证件照

3. ready player me 生成glb模型



如上图,选择一个图片之后ready player me会自动生成一个模型,可选择搭配预设的发型、着装等,然后点击右上角的next按钮
点击复制按钮,下载glb模型
4. 使用blender 将 glb转为fbx模型,此处有坑
新建项目-按A键全选-删除-文件-导入-选择glb模型
导入后选择这些球球,按delete删除
然后这个棱角球和他的父级也要删除,先删除菱角球再删父级 
导出之前要在着色里做一下操作,要不导出之后可能没有贴图,右上角圈出来的每一个都要这么做
文件-导出-fbx,路径模式要选择复制并且旁边的按钮要点亮,要不导出之后没有贴图和材质
5. mixamo绑定动作

等待解析
点击next, 此处会发现模型质感不好,这是默认光照的问题,不用担心,后续展示的时候关照可调
此处将圆圈分别拖到对应的关节处,然后点next
点next
点next
选择一个动作,然后点download
点download,下载完之后可以二次加工或者直接用于展示,本文直接用于展示
6. 使用vue3展示fbx模型,参考代码如下(请将modelUrl 替换为你的文件url[本地或者服务器都行]):
<template>
<div class="mt-4"></div>
<span class="ml-4" @click="changeRole(1)">角色1</span>
<span class="ml-4" @click="changeRole(2)">角色2</span>
<span class="ml-4" @click="changeRole(3)">角色3</span>
<div ref="container" class="viewer-container"></div>
</template>
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import * as THREE from 'three'
import {OrbitControls} from 'three-stdlib'
import {FBXLoader} from 'three-stdlib'
// DOM 容器
const container = ref(null)
// 当前模型 URL
const modelUrl = ref('https://xxx:5102/xxx/lwq.fbx')
// 持久变量
let scene, renderer, camera, controls, mixer, clock, wrapper
const loader = new FBXLoader()
// 初始化一次渲染器和基本场景
const initScene = () => {
scene = new THREE.Scene()
renderer = new THREE.WebGLRenderer({alpha: true, antialias: true})
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setClearColor(0x000000, 0)
renderer.outputEncoding = THREE.sRGBEncoding
container.value.appendChild(renderer.domElement)
camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 100)
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.enablePan = false
controls.minDistance = 2
controls.maxDistance = 10
controls.maxPolarAngle = Math.PI / 1.8
// 灯光
scene.add(new THREE.AmbientLight(0xffffff, 1.0))
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5)
hemiLight.position.set(0, 20, 0)
scene.add(hemiLight)
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0)
dirLight.position.set(3, 5, 4)
scene.add(dirLight)
const frontLight = new THREE.DirectionalLight(0xffffff, 0.8)
frontLight.position.set(0, 1, 2)
scene.add(frontLight)
clock = new THREE.Clock()
animate()
}
// 加载模型(移除旧模型,加入新模型)
const loadModel = (url) => {
// 清除旧模型
if (wrapper) {
scene.remove(wrapper)
wrapper.traverse(child => {
if (child.isMesh) {
child.geometry.dispose()
if (child.material.map) child.material.map.dispose()
child.material.dispose()
}
})
wrapper = null
}
mixer = null
loader.load(
url,
(fbx) => {
fbx.scale.set(0.0045, 0.0045, 0.0045)
fbx.rotateX(-Math.PI / 2)
fbx.traverse((child) => {
if (child.isMesh && child.material) {
const old = child.material
const mat = new THREE.MeshStandardMaterial({
map: old.map || null,
color: old.color || new THREE.Color(0xffffff),
roughness: 0.5,
metalness: 0.0,
transparent: old.transparent || false,
opacity: old.opacity ?? 1.0
})
mat.envMapIntensity = 0
mat.needsUpdate = true
child.material = mat
}
})
const fbxBox = new THREE.Box3().setFromObject(fbx)
const fbxCenter = fbxBox.getCenter(new THREE.Vector3())
const fbxSize = fbxBox.getSize(new THREE.Vector3())
wrapper = new THREE.Group()
wrapper.add(fbx)
fbx.position.sub(fbxCenter)
wrapper.rotation.x = Math.PI / 2
wrapper.rotation.z = -Math.PI / 6
scene.add(wrapper)
const maxDim = Math.max(fbxSize.x, fbxSize.y, fbxSize.z)
const distance = maxDim * 1.8
const angle = Math.PI / 6
const camX = distance * Math.cos(angle)
const camY = maxDim * 0.3
const camZ = distance * Math.sin(angle)
camera.position.set(camX, camY, camZ)
camera.lookAt(new THREE.Vector3(0, 0, 0))
controls.target.set(0, 0, 0)
controls.update()
if (fbx.animations.length > 0) {
mixer = new THREE.AnimationMixer(fbx)
mixer.clipAction(fbx.animations[0]).play()
}
console.log('✅ 模型加载成功:', url)
},
undefined,
(err) => {
console.error('❌ 模型加载失败:', err)
}
)
}
// 动画循环
const animate = () => {
requestAnimationFrame(animate)
const delta = clock.getDelta()
if (mixer) mixer.update(delta)
controls.update()
renderer.render(scene, camera)
}
// 切换角色
const changeRole = (roleId) => {
console.log(`切换到角色 ${roleId}`)
if (roleId === 1) {
modelUrl.value = 'https://xxx.com:5102/xxx/lwq.fbx'
} else if (roleId === 2) {
modelUrl.value = 'https://xxx:5102/xxx/zzy.fbx'
} else if (roleId === 3) {
modelUrl.value = 'https://xxx:5102/xxx/zjh.fbx'
}
loadModel(modelUrl.value)
}
// 初始化
onMounted(() => {
initScene()
loadModel(modelUrl.value)
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
})
</script>
<style scoped>
.viewer-container {
width: 100vw;
height: 100vh;
overflow: hidden;
background: transparent;
}
</style>