AR 开发者必备 3D 建模:Blender 零基础到商业级模型输出全流程
Blender 建模→.glb 导出→Three.js 集成→AR 交互” 的完整闭环 —— 既解决了 AR 开发者 “模型从哪来” 的痛点,又通过实战代码解决了 “模型怎么用” 的核心需求。核心亮点在于:Blender 建模无需美术基础,Three.js 代码可直接复用,个人开发者无需依赖专业团队,即可独立完成 AR 模型的制作与应用落地。掌握这套 “建模 + 代码” 一体化技能后,你将能快速响

📚所属栏目:python

开篇:3D 模型是 AR 应用的 “内容核心”,解决 “虚拟内容从哪来” 的痛点
AR 应用的沉浸感离不开高质量 3D 模型,但多数个人开发者面临两大困境:商业 3D 模型版权贵、定制化程度低,免费模型精度差、格式不兼容;而专业建模软件(如 3ds Max、Maya)学习成本高、付费门槛高,难以快速上手。
本期我们将聚焦 “AR 场景轻量化 3D 建模 + 代码集成”,以 Blender(免费开源)为工具,从零基础拆解 AR 模型建模逻辑,再通过 Three.js 将 Blender 导出的.glb 模型集成到 AR 场景,实现 “模型加载→姿态校准→点击交互→动态更新” 全流程。全程以 “AR 虚拟导览牌” 为实战案例,覆盖从 Blender 建模到 AR 应用落地的完整链路,既解决 “模型从哪来”,又解决 “模型怎么用”,个人开发者可直接复用建模流程和代码,快速落地 AR 项目。
一、AR 场景 3D 模型的核心要求与工具选型
1. AR 模型与传统 3D 模型的核心差异
AR 模型需适配移动端 / AR 设备(如 Rokid 眼镜)的算力限制,核心要求可总结为 “轻、准、兼容、可交互”:
- 轻量性:面数≤5000(复杂模型≤10000),避免设备渲染卡顿;
- 精准性:尺寸比例符合真实场景(如虚拟导览牌高度 1.5 米,与真实导览牌一致);
- 兼容性:优先使用.glb/.gltf 格式(支持纹理嵌入、体积小、AR 引擎原生支持);
- 可交互性:模型结构清晰(避免复杂嵌套),便于 AR 场景中射线检测、点击响应。
2. 工具选型:Blender+Three.js(建模 + 集成闭环)
| 工具模块 | 选型方案 | 核心作用 | 核心优势 |
|---|---|---|---|
| 3D 建模工具 | Blender 4.0+ | 制作轻量化 AR 模型,导出.glb 格式 | 免费开源、功能全面、支持 AR 模型优化导出 |
| AR 集成引擎 | Three.js + WebXR | 加载.glb 模型,实现 AR 叠加与交互 | 浏览器原生支持、无需安装、适配手机相机 |
| 模型优化工具 | glTF Transform | 压缩.glb 模型体积,提升加载速度 | 支持 mesh 压缩、纹理压缩,体积减少 50%+ |
| 交互辅助工具 | Raycaster(Three.js 内置) | 实现 AR 模型点击、选中交互 | 轻量高效,适配移动端触摸操作 |
二、实战案例:AR 虚拟导览牌建模(Blender 流程)
(原有建模流程不变,快速回顾核心步骤)
- 草图规划:底座(0.3 米高)+ 立杆(1.2 米高)+ 牌面(0.8×0.5 米)+ 文字 / LOGO;
- 低面数建模:用立方体 / 圆柱体搭建主体,文字转网格嵌入牌面,面数控制在 3000 以内;
- UV 展开与纹理:智能 UV 展开,用 Canva 制作轻量化纹理(1024×1024,WebP 格式);
- 导出.glb 格式:勾选 “Embed Textures”(嵌入纹理),用 glTF Transform 压缩模型。
最终模型参数(确保代码集成兼容性)
- 格式:.glb(单个文件,大小 1.2MB);
- 面数:2780;
- 纹理:嵌入 1 张 1024×1024 WebP 格式纹理;
- 原点:模型原点对齐底座中心(便于 AR 场景中定位)。
三、核心代码:AR 场景集成.glb 模型(Three.js 实现)
1. 开发环境配置
- 核心库:Three.js(0.160.0)、GLTFLoader(Three.js 内置)、WebXR(Three.js 内置);
- 开发工具:VS Code + Live Server(本地调试);
- 依赖引入(CDN 方式,无需 npm 安装):
<!-- 引入Three.js核心库 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<!-- 引入.glb模型加载器 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/loaders/GLTFLoader.js"></script>
<!-- 引入WebXR支持 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/webxr/WebXRManager.js"></script>
2. 核心代码:AR 场景初始化 + 模型加载
// 全局变量初始化
let scene, camera, renderer, arSession, raycaster, touchVector;
let guideModel = null; // 虚拟导览牌模型实例
const MODEL_PATH = './models/ar_guide_sign.glb'; // Blender导出的.glb模型路径
const AR_MARKER_HEIGHT = 1.5; // 虚拟导览牌在AR场景中的高度(与真实世界一致)
// 初始化Three.js场景
function initScene() {
// 1. 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000, 0); // 透明背景,叠加相机画面
// 2. 创建透视相机(适配手机屏幕)
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 3. 创建WebGL渲染器(启用AR支持)
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: 'high-performance' // 优先高性能渲染
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true; // 启用WebXR
document.body.appendChild(renderer.domElement);
// 4. 添加光照(确保模型纹理清晰可见)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(0, 1, 1);
scene.add(ambientLight, directionalLight);
// 5. 初始化射线检测(用于模型点击交互)
raycaster = new THREE.Raycaster();
touchVector = new THREE.Vector2();
// 6. 绑定窗口大小适配事件
window.addEventListener('resize', onWindowResize);
}
// 加载.glb虚拟导览牌模型
function loadGuideModel() {
const loader = new GLTFLoader();
loader.load(
MODEL_PATH,
(gltf) => {
// 1. 获取模型主体(Blender中合并的整体模型)
guideModel = gltf.scene;
// 2. 模型缩放与姿态校准(匹配真实世界尺寸)
guideModel.scale.set(1, 1, 1); // Blender中已按1:1米制作,无需额外缩放
guideModel.rotation.y = Math.PI; // 调整模型朝向(根据Blender导出姿态适配)
// 3. 设置模型初始位置(AR场景中地面上方1.5米)
guideModel.position.set(0, -AR_MARKER_HEIGHT, -3); // 初始在相机前方3米处
// 4. 启用模型阴影(提升真实感)
guideModel.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
// 5. 将模型添加到场景
scene.add(guideModel);
console.log('虚拟导览牌模型加载成功');
},
(xhr) => {
// 加载进度回调
console.log(`模型加载中:${(xhr.loaded / xhr.total) * 100}%`);
},
(error) => {
// 加载失败处理
console.error('模型加载失败:', error);
alert('虚拟导览牌加载失败,请检查模型路径或网络');
}
);
}
// 窗口大小适配
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
3. 核心代码:AR 相机启动 + 模型定位
// 启动AR相机(获取手机摄像头画面)
async function startARCamera() {
try {
// 1. 请求AR会话(WebXR API)
arSession = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['camera-access']
});
// 2. 将AR会话绑定到渲染器
await renderer.xr.setSession(arSession);
// 3. 监听AR会话状态变化
arSession.addEventListener('end', () => {
console.log('AR会话结束');
alert('AR模式已关闭,请重新启动');
});
console.log('AR相机启动成功');
} catch (error) {
console.error('AR相机启动失败:', error);
// 降级处理:若浏览器不支持WebXR,使用普通相机画面作为背景
startNormalCamera();
}
}
// 降级方案:普通相机背景(适配不支持WebXR的浏览器)
function startNormalCamera() {
const video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.muted = true;
// 获取设备相机流
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
.then((stream) => {
video.srcObject = stream;
// 将视频作为场景背景纹理
const videoTexture = new THREE.VideoTexture(video);
scene.background = videoTexture;
console.log('普通相机启动成功(WebXR不支持)');
})
.catch((error) => {
console.error('普通相机启动失败:', error);
alert('请授予相机权限,否则无法使用AR功能');
});
}
4. 核心代码:模型交互(点击显示导览信息)
// 绑定触摸/点击事件(移动端+桌面端适配)
function bindInteractionEvents() {
// 移动端触摸事件
document.addEventListener('touchstart', onTouchStart);
// 桌面端点击事件(调试用)
document.addEventListener('click', onMouseClick);
}
// 移动端触摸处理
function onTouchStart(event) {
// 将触摸坐标转换为Three.js标准化设备坐标
const touch = event.touches[0];
touchVector.x = (touch.clientX / window.innerWidth) * 2 - 1;
touchVector.y = -(touch.clientY / window.innerHeight) * 2 + 1;
detectModelClick();
}
// 桌面端点击处理(调试用)
function onMouseClick(event) {
touchVector.x = (event.clientX / window.innerWidth) * 2 - 1;
touchVector.y = -(event.clientY / window.innerHeight) * 2 + 1;
detectModelClick();
}
// 检测是否点击到虚拟导览牌
function detectModelClick() {
if (!guideModel) return;
// 更新射线检测的射线方向(从相机指向点击位置)
raycaster.setFromCamera(touchVector, camera);
// 检测射线与模型的交点
const intersects = raycaster.intersectObject(guideModel, true);
if (intersects.length > 0) {
// 点击到模型:显示导览信息
showGuideInfo();
// 模型点击反馈:轻微缩放
guideModel.scale.set(1.05, 1.05, 1.05);
setTimeout(() => {
guideModel.scale.set(1, 1, 1);
}, 300);
}
}
// 显示导览信息(AR场景中叠加虚拟弹窗)
function showGuideInfo() {
// 1. 创建信息面板(Three.js Canvas纹理)
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 200;
// 2. 绘制信息面板背景与文字
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.fillText('展厅导览', canvas.width / 2, 50);
ctx.font = '16px Arial';
ctx.fillStyle = '#e5e7eb';
ctx.fillText('左侧:历史文物区(10米)', canvas.width / 2, 90);
ctx.fillText('右侧:现代科技区(5米)', canvas.width / 2, 120);
ctx.fillText('前方:互动体验区(15米)', canvas.width / 2, 150);
// 3. 创建Three.js纹理与面板模型
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true
});
const geometry = new THREE.PlaneGeometry(0.6, 0.3);
const infoPanel = new THREE.Mesh(geometry, material);
// 4. 将面板放在导览牌上方
infoPanel.position.set(0, 0.5, 0);
infoPanel.lookAt(camera.position); // 面板始终面向相机
// 5. 添加到场景并设置自动移除
guideModel.add(infoPanel);
setTimeout(() => {
infoPanel.removeFromParent();
}, 5000); // 5秒后自动隐藏
}
5. 核心代码:动画循环与场景渲染
// 动画循环(实时更新场景)
function animate() {
requestAnimationFrame(animate);
// 若模型已加载,让模型轻微旋转(提升视觉吸引力)
if (guideModel) {
guideModel.rotation.y += 0.005;
}
// 渲染AR场景
renderer.render(scene, camera);
}
// 初始化入口函数
async function init() {
initScene(); // 初始化Three.js场景
loadGuideModel(); // 加载.glb模型
await startARCamera(); // 启动AR相机
bindInteractionEvents(); // 绑定交互事件
animate(); // 启动动画循环
}
// 页面加载完成后初始化
window.onload = init;
6. 完整 HTML 页面(整合所有代码)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AR虚拟导览牌(Blender建模+Three.js集成)</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
.loading-tips {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ffffff;
font-size: 18px;
z-index: 100;
}
</style>
</head>
<body>
<div class="loading-tips">AR导览启动中...请授予相机权限</div>
<!-- 引入Three.js相关库 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/webxr/WebXRManager.js"></script>
<script>
// 全局变量初始化
let scene, camera, renderer, arSession, raycaster, touchVector;
let guideModel = null;
const MODEL_PATH = './models/ar_guide_sign.glb';
const AR_MARKER_HEIGHT = 1.5;
// 初始化Three.js场景
function initScene() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000, 0);
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: 'high-performance'
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(0, 1, 1);
scene.add(ambientLight, directionalLight);
raycaster = new THREE.Raycaster();
touchVector = new THREE.Vector2();
window.addEventListener('resize', onWindowResize);
}
// 加载.glb模型
function loadGuideModel() {
const loader = new GLTFLoader();
loader.load(
MODEL_PATH,
(gltf) => {
guideModel = gltf.scene;
guideModel.scale.set(1, 1, 1);
guideModel.rotation.y = Math.PI;
guideModel.position.set(0, -AR_MARKER_HEIGHT, -3);
guideModel.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(guideModel);
document.querySelector('.loading-tips').style.display = 'none';
console.log('模型加载成功');
},
(xhr) => {
console.log(`加载中:${(xhr.loaded / xhr.total) * 100}%`);
},
(error) => {
console.error('模型加载失败:', error);
document.querySelector('.loading-tips').textContent = '模型加载失败,请刷新重试';
}
);
}
// 启动AR相机
async function startARCamera() {
try {
arSession = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['camera-access']
});
await renderer.xr.setSession(arSession);
arSession.addEventListener('end', () => {
alert('AR模式已关闭,请重新启动');
});
} catch (error) {
console.error('AR相机启动失败:', error);
startNormalCamera();
}
}
// 降级:普通相机背景
function startNormalCamera() {
const video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.muted = true;
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
.then((stream) => {
video.srcObject = stream;
const videoTexture = new THREE.VideoTexture(video);
scene.background = videoTexture;
})
.catch((error) => {
console.error('普通相机启动失败:', error);
document.querySelector('.loading-tips').textContent = '请授予相机权限';
});
}
// 交互事件绑定
function bindInteractionEvents() {
document.addEventListener('touchstart', onTouchStart);
document.addEventListener('click', onMouseClick);
}
function onTouchStart(event) {
const touch = event.touches[0];
touchVector.x = (touch.clientX / window.innerWidth) * 2 - 1;
touchVector.y = -(touch.clientY / window.innerHeight) * 2 + 1;
detectModelClick();
}
function onMouseClick(event) {
touchVector.x = (event.clientX / window.innerWidth) * 2 - 1;
touchVector.y = -(event.clientY / window.innerHeight) * 2 + 1;
detectModelClick();
}
function detectModelClick() {
if (!guideModel) return;
raycaster.setFromCamera(touchVector, camera);
const intersects = raycaster.intersectObject(guideModel, true);
if (intersects.length > 0) {
showGuideInfo();
guideModel.scale.set(1.05, 1.05, 1.05);
setTimeout(() => {
guideModel.scale.set(1, 1, 1);
}, 300);
}
}
function showGuideInfo() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 200;
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.fillText('展厅导览', canvas.width / 2, 50);
ctx.font = '16px Arial';
ctx.fillStyle = '#e5e7eb';
ctx.fillText('左侧:历史文物区(10米)', canvas.width / 2, 90);
ctx.fillText('右侧:现代科技区(5米)', canvas.width / 2, 120);
ctx.fillText('前方:互动体验区(15米)', canvas.width / 2, 150);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true });
const geometry = new THREE.PlaneGeometry(0.6, 0.3);
const infoPanel = new THREE.Mesh(geometry, material);
infoPanel.position.set(0, 0.5, 0);
infoPanel.lookAt(camera.position);
guideModel.add(infoPanel);
setTimeout(() => {
infoPanel.removeFromParent();
}, 5000);
}
// 窗口适配与动画循环
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
if (guideModel) {
guideModel.rotation.y += 0.005;
}
renderer.render(scene, camera);
}
// 初始化入口
async function init() {
initScene();
loadGuideModel();
await startARCamera();
bindInteractionEvents();
animate();
}
window.onload = init;
</script>
</body>
</html>
四、代码运行与模型优化关键技巧
1. 本地运行步骤
- 文件夹结构:
ar-guide-sign/
├── index.html # 主页面(整合所有代码)
└── models/
└── ar_guide_sign.glb # Blender导出的模型文件
- 用 VS Code 打开文件夹,安装 “Live Server” 插件;
- 右键点击
index.html,选择 “Open with Live Server”; - 手机扫码(Live Server 生成的局域网链接),授予相机权限即可运行。
2. 模型加载优化(解决卡顿 / 加载慢)
- 模型压缩:用 glTF Transform 进一步压缩.glb 模型,命令如下:
# 安装glTF Transform
npm install -g @gltf-transform/cli
# 压缩模型(体积减少50%+)
gltf-transform compress ./models/ar_guide_sign.glb ./models/ar_guide_sign_compressed.glb --compress meshopt --texture-compress webp
- 懒加载策略:先加载低精度模型占位,再加载高精度模型:
// 简化版:先加载低面数占位模型,再替换为高精度模型
function loadModelWithLazy() {
// 1. 加载低面数占位模型(100面以内,体积<100KB)
const lazyLoader = new GLTFLoader();
lazyLoader.load('./models/guide_sign_lazy.glb', (lazyGltf) => {
scene.add(lazyGltf.scene);
// 2. 后台加载高精度模型
const highLoader = new GLTFLoader();
highLoader.load('./models/ar_guide_sign_compressed.glb', (highGltf) => {
// 替换占位模型
scene.remove(lazyGltf.scene);
scene.add(highGltf.scene);
});
});
}
3. 常见问题排查(代码层面)
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 模型加载失败 | 模型路径错误、格式不兼容 | 检查 MODEL_PATH 是否正确;用 Blender 重新导出.glb(勾选 Embed Textures) |
| 模型显示异常(拉伸 / 翻转) | Blender 中模型缩放 / 旋转不当 | 在 Blender 中重置模型变换(Ctrl+A → Scale/Rotation);代码中调整 scale/rotation |
| 点击模型无响应 | 射线检测未包含子网格、模型位置过远 | 射线检测时设置 recursive=true(intersectObject (guideModel, true));调整模型 position |
| 移动端卡顿 | 模型面数超标、渲染压力大 | 面数控制在 3000 以内;关闭抗锯齿(antialias: false);降低纹理分辨率至 512×512 |
五、商业变现路径(建模 + 代码一体化接单)
1. 一体化接单报价参考(模型 + 代码集成)
| 服务类型 | 包含内容 | 报价范围(元) | 交付周期 |
|---|---|---|---|
| 单个 AR 模型 + 集成 | Blender 建模 + 纹理 + Three.js 加载代码 + 交互逻辑 | 1500-3000 | 3-5 天 |
| 整套 AR 导览系统 | 10 个以内模型 + 场景集成 + 路径规划 + 后台管理 | 10000-30000 | 2-4 周 |
| 模型定制 + 引擎适配 | 定制化模型 + Unity/Unreal 集成代码 | 3000-8000 | 5-7 天 |
2. 代码复用技巧(提升接单效率)
- 封装模型加载工具类:将.glb 加载、缩放、姿态校准封装为通用函数,适配不同模型:
// 通用模型加载工具类
class ARModelLoader {
static loadModel(path, scale = 1, rotation = { x: 0, y: 0, z: 0 }) {
return new Promise((resolve, reject) => {
const loader = new GLTFLoader();
loader.load(path, (gltf) => {
const model = gltf.scene;
model.scale.set(scale, scale, scale);
model.rotation.set(rotation.x, rotation.y, rotation.z);
resolve(model);
}, null, reject);
});
}
}
// 复用调用
ARModelLoader.loadModel('./models/another_model.glb', 0.8, { y: Math.PI/2 })
.then((model) => {
scene.add(model);
});
六、总结与下期预告
“Blender 建模→.glb 导出→Three.js 集成→AR 交互” 的完整闭环 —— 既解决了 AR 开发者 “模型从哪来” 的痛点,又通过实战代码解决了 “模型怎么用” 的核心需求。核心亮点在于:Blender 建模无需美术基础,Three.js 代码可直接复用,个人开发者无需依赖专业团队,即可独立完成 AR 模型的制作与应用落地。
掌握这套 “建模 + 代码” 一体化技能后,你将能快速响应 AR 项目的定制化需求,无论是自用还是接单,都能大幅提升效率、降低成本。需要注意的是,AR 模型的核心是 “轻量化 + 兼容性”,代码集成时需重点关注加载速度和交互流畅度,优先适配移动端设备。
更多推荐



所有评论(0)