码上灵感
  • 经验

    • 服务器

      • 操作系统
      • 网络配置
      • ssh配置
      • 压力测试
      • IPMI
    • 基础工具

      • xshell
      • xftp
    • 基础环境

      • anaconda
      • v2ray
      • cuda
      • docker
      • nginx
      • ufw
      • git lfs 大文件
    • ubuntu

      • virtualbox
    • minio

      • 安装
    • postgres

      • 安装
    • AI相关折腾

      • maxkb
      • huggingface
      • vllm
      • ktransformers
      • wan2.1
    • 疑难杂症

      • ubuntu自动更新
      • xshell隧道转发失败
      • video自动播放不成功的问题
      • 3d模型快速生成

3d模型快速生成

一、概述

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

效果:

步骤图片

涉及到的工具:

chatgpt或者其他AI大语言模型 https://chatgpt.com/

即梦

https://jimeng.jianying.com/

Ready Player Me

https://readyplayer.me/

Blender

https://www.blender.org/

Mixamo

https://www.mixamo.com/

vue3

https://vuejs.org/


二、具体步骤

步骤图片

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>
Prev
video自动播放不成功的问题