我将创建一个交互式的Three.js自定义模型解析演示,展示如何加载、解析和操作自定义3D模型。这个系统将包含模型加载、几何数据可视化、实时编辑等功能。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 自定义模型解析系统</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/OBJLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/MTLLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #e6e6e6;
min-height: 100vh;
overflow-x: hidden;
}
.container {
display: flex;
flex-direction: column;
max-width: 1600px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 30px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 30px;
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
background: linear-gradient(90deg, #4cc9f0, #4361ee);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 2px 10px rgba(76, 201, 240, 0.3);
}
.subtitle {
font-size: 1.2rem;
color: #a0a0c0;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
}
.content {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-bottom: 40px;
}
.viewer-container {
flex: 1;
min-width: 300px;
min-height: 500px;
background: rgba(10, 15, 30, 0.7);
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
position: relative;
border: 1px solid rgba(76, 201, 240, 0.2);
}
#model-viewer {
width: 100%;
height: 500px;
display: block;
}
.controls-panel {
flex: 1;
min-width: 300px;
background: rgba(15, 23, 42, 0.8);
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(67, 97, 238, 0.2);
}
.panel-title {
font-size: 1.5rem;
margin-bottom: 25px;
color: #4cc9f0;
padding-bottom: 10px;
border-bottom: 1px solid rgba(76, 201, 240, 0.3);
}
.control-group {
margin-bottom: 25px;
}
.control-label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: #b8c1ec;
}
.slider-container {
display: flex;
align-items: center;
gap: 15px;
}
.slider-value {
min-width: 40px;
text-align: center;
background: rgba(76, 201, 240, 0.1);
padding: 5px 10px;
border-radius: 5px;
font-weight: 600;
}
input[type="range"] {
flex: 1;
height: 8px;
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #4361ee;
cursor: pointer;
box-shadow: 0 0 10px rgba(67, 97, 238, 0.7);
}
.btn {
display: inline-block;
background: linear-gradient(90deg, #4361ee, #3a0ca3);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-right: 10px;
margin-bottom: 10px;
box-shadow: 0 5px 15px rgba(67, 97, 238, 0.3);
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(67, 97, 238, 0.5);
}
.btn:active {
transform: translateY(1px);
}
.btn-outline {
background: transparent;
border: 2px solid #4361ee;
color: #4361ee;
}
.model-info {
background: rgba(10, 15, 30, 0.8);
border-radius: 15px;
padding: 25px;
margin-top: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(76, 201, 240, 0.2);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.info-item {
background: rgba(20, 30, 60, 0.5);
padding: 15px;
border-radius: 10px;
border-left: 4px solid #4cc9f0;
}
.info-label {
font-size: 0.9rem;
color: #a0a0c0;
margin-bottom: 5px;
}
.info-value {
font-size: 1.2rem;
font-weight: 600;
color: #ffffff;
}
.toggle-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.toggle-btn {
background: rgba(67, 97, 238, 0.2);
color: #b8c1ec;
border: 1px solid rgba(67, 97, 238, 0.5);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.toggle-btn.active {
background: rgba(67, 97, 238, 0.8);
color: white;
border-color: #4361ee;
}
.toggle-btn:hover {
background: rgba(67, 97, 238, 0.4);
}
.status-bar {
background: rgba(10, 15, 30, 0.9);
padding: 15px;
border-radius: 10px;
margin-top: 30px;
border-left: 4px solid #f72585;
display: flex;
align-items: center;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #4cc9f0;
margin-right: 15px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.7; }
50% { opacity: 1; }
100% { opacity: 0.7; }
}
footer {
text-align: center;
padding: 30px 0;
margin-top: 30px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: #a0a0c0;
font-size: 0.9rem;
}
.highlight {
color: #4cc9f0;
font-weight: 600;
}
@media (max-width: 768px) {
.content {
flex-direction: column;
}
h1 {
font-size: 2.2rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Three.js 自定义模型解析系统</h1>
<p class="subtitle">本演示展示了Three.js如何解析和操作自定义3D模型。您可以加载预定义模型,查看其几何数据,并实时调整模型属性。</p>
</header>
<div class="content">
<div class="viewer-container">
<canvas id="model-viewer"></canvas>
</div>
<div class="controls-panel">
<h2 class="panel-title">模型控制</h2>
<div class="control-group">
<label class="control-label">选择模型类型</label>
<div class="toggle-group">
<div class="toggle-btn active" data-model="sphere">球体</div>
<div class="toggle-btn" data-model="cube">立方体</div>
<div class="toggle-btn" data-model="torus">圆环</div>
<div class="toggle-btn" data-model="custom">自定义网格</div>
</div>
</div>
<div class="control-group">
<label class="control-label">旋转速度</label>
<div class="slider-container">
<input type="range" id="rotation-speed" min="0" max="5" step="0.1" value="1">
<span class="slider-value" id="rotation-value">1.0</span>
</div>
</div>
<div class="control-group">
<label class="control-label">模型尺寸</label>
<div class="slider-container">
<input type="range" id="model-scale" min="0.5" max="3" step="0.1" value="1">
<span class="slider-value" id="scale-value">1.0</span>
</div>
</div>
<div class="control-group">
<label class="control-label">网格线框</label>
<div class="slider-container">
<input type="range" id="wireframe-opacity" min="0" max="1" step="0.1" value="0.5">
<span class="slider-value" id="wireframe-value">0.5</span>
</div>
</div>
<div class="control-group">
<label class="control-label">顶点大小</label>
<div class="slider-container">
<input type="range" id="vertex-size" min="0" max="0.2" step="0.01" value="0.05">
<span class="slider-value" id="vertex-value">0.05</span>
</div>
</div>
<button id="reset-btn" class="btn">重置设置</button>
<button id="export-btn" class="btn btn-outline">导出几何数据</button>
<button id="add-light" class="btn">添加光源</button>
</div>
</div>
<div class="model-info">
<h2 class="panel-title">模型信息</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">模型类型</div>
<div class="info-value" id="info-type">球体</div>
</div>
<div class="info-item">
<div class="info-label">顶点数量</div>
<div class="info-value" id="info-vertices">482</div>
</div>
<div class="info-item">
<div class="info-label">三角面数量</div>
<div class="info-value" id="info-faces">960</div>
</div>
<div class="info-item">
<div class="info-label">边界框尺寸</div>
<div class="info-value" id="info-bounds">2.0 x 2.0 x 2.0</div>
</div>
</div>
</div>
<div class="status-bar">
<div class="status-indicator"></div>
<div id="status-text">系统就绪。选择模型类型开始探索Three.js自定义模型解析功能。</div>
</div>
<footer>
<p>Three.js 自定义模型解析演示 | 使用 <span class="highlight">Three.js r128</span> 构建 | 模型数据实时解析与可视化</p>
</footer>
</div>
<script>
// 全局变量
let scene, camera, renderer, controls;
let currentModel = null;
let modelGroup = null;
let wireframe = null;
let vertices = null;
let rotationSpeed = 1.0;
let modelScale = 1.0;
let wireframeOpacity = 0.5;
let vertexSize = 0.05;
// 初始化Three.js场景
function init() {
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0f1e);
// 创建相机
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
camera.position.set(5, 5, 5);
// 创建渲染器
const canvas = document.getElementById('model-viewer');
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// 添加轨道控制
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
// 添加平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 15);
scene.add(directionalLight);
// 创建模型组
modelGroup = new THREE.Group();
scene.add(modelGroup);
// 初始化加载默认模型
loadModel('sphere');
// 窗口大小调整事件
window.addEventListener('resize', onWindowResize);
// 更新状态信息
updateModelInfo();
// 开始动画循环
animate();
}
// 加载模型函数
function loadModel(modelType) {
// 清除之前的模型
while(modelGroup.children.length > 0) {
modelGroup.remove(modelGroup.children[0]);
}
let geometry, material;
// 根据类型创建几何体
switch(modelType) {
case 'sphere':
geometry = new THREE.SphereGeometry(1, 32, 32);
material = new THREE.MeshPhongMaterial({
color: 0x4361ee,
shininess: 100,
transparent: true,
opacity: 0.9
});
break;
case 'cube':
geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
material = new THREE.MeshPhongMaterial({
color: 0x3a0ca3,
shininess: 80,
transparent: true,
opacity: 0.9
});
break;
case 'torus':
geometry = new THREE.TorusGeometry(1, 0.4, 16, 100);
material = new THREE.MeshPhongMaterial({
color: 0x7209b7,
shininess: 120,
transparent: true,
opacity: 0.9
});
break;
case 'custom':
// 创建自定义几何体(复杂形状)
geometry = new THREE.IcosahedronGeometry(1, 2);
material = new THREE.MeshPhongMaterial({
color: 0xf72585,
shininess: 150,
transparent: true,
opacity: 0.9
});
break;
}
// 创建模型网格
currentModel = new THREE.Mesh(geometry, material);
modelGroup.add(currentModel);
// 创建线框
const wireframeGeometry = new THREE.WireframeGeometry(geometry);
wireframe = new THREE.LineSegments(wireframeGeometry);
wireframe.material.depthTest = false;
wireframe.material.opacity = wireframeOpacity;
wireframe.material.transparent = true;
wireframe.material.color = new THREE.Color(0x4cc9f0);
wireframe.visible = true;
modelGroup.add(wireframe);
// 创建顶点可视化
const verticesGeometry = new THREE.BufferGeometry();
verticesGeometry.setAttribute('position', geometry.attributes.position);
const verticesMaterial = new THREE.PointsMaterial({
color: 0xffea00,
size: vertexSize,
sizeAttenuation: true
});
vertices = new THREE.Points(verticesGeometry, verticesMaterial);
modelGroup.add(vertices);
// 更新模型信息
updateModelInfo();
// 更新状态
updateStatus(`已加载${getModelName(modelType)}模型,包含${geometry.attributes.position.count}个顶点`);
}
// 获取模型名称
function getModelName(type) {
const names = {
'sphere': '球体',
'cube': '立方体',
'torus': '圆环',
'custom': '自定义二十面体'
};
return names[type] || type;
}
// 更新模型信息面板
function updateModelInfo() {
if (!currentModel) return;
const geometry = currentModel.geometry;
const verticesCount = geometry.attributes.position.count;
const facesCount = geometry.index ? geometry.index.count / 3 : verticesCount / 3;
// 计算边界框
geometry.computeBoundingBox();
const bounds = geometry.boundingBox;
const boundsSize = bounds.getSize(new THREE.Vector3());
// 更新显示信息
document.getElementById('info-type').textContent = getModelName(getCurrentModelType());
document.getElementById('info-vertices').textContent = verticesCount;
document.getElementById('info-faces').textContent = facesCount;
document.getElementById('info-bounds').textContent =
`${boundsSize.x.toFixed(2)} x ${boundsSize.y.toFixed(2)} x ${boundsSize.z.toFixed(2)}`;
}
// 获取当前模型类型
function getCurrentModelType() {
const activeBtn = document.querySelector('.toggle-btn.active');
return activeBtn ? activeBtn.getAttribute('data-model') : 'sphere';
}
// 更新状态信息
function updateStatus(message) {
document.getElementById('status-text').textContent = message;
}
// 窗口大小调整处理
function onWindowResize() {
const canvas = document.getElementById('model-viewer');
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 旋转模型
if (modelGroup && rotationSpeed > 0) {
modelGroup.rotation.y += 0.01 * rotationSpeed;
modelGroup.rotation.x += 0.005 * rotationSpeed;
}
// 更新控制
controls.update();
// 渲染场景
renderer.render(scene, camera);
}
// 导出几何数据
function exportGeometryData() {
if (!currentModel) return;
const geometry = currentModel.geometry;
const vertices = geometry.attributes.position.array;
const indices = geometry.index ? geometry.index.array : null;
let data = `Three.js 模型几何数据导出\n`;
data += `模型类型: ${getModelName(getCurrentModelType())}\n`;
data += `顶点数量: ${vertices.length / 3}\n`;
data += `索引数量: ${indices ? indices.length : '无索引'}\n\n`;
data += `顶点数据 (x, y, z):\n`;
// 添加前10个顶点作为示例
for (let i = 0; i < Math.min(vertices.length, 30); i += 3) {
data += `v${i/3}: ${vertices[i].toFixed(4)}, ${vertices[i+1].toFixed(4)}, ${vertices[i+2].toFixed(4)}\n`;
}
if (indices) {
data += `\n索引数据 (三角面):\n`;
// 添加前10个索引作为示例
for (let i = 0; i < Math.min(indices.length, 30); i += 3) {
data += `f${i/3}: ${indices[i]}, ${indices[i+1]}, ${indices[i+2]}\n`;
}
}
// 创建下载
const blob = new Blob([data], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `threejs-model-${getCurrentModelType()}-data.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
updateStatus(`已导出${getModelName(getCurrentModelType())}模型的几何数据`);
}
// 添加额外光源
function addLight() {
const light = new THREE.PointLight(0xff9000, 1, 20);
light.position.set(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10 + 5,
(Math.random() - 0.5) * 10
);
scene.add(light);
// 添加光源可视化
const lightHelper = new THREE.PointLightHelper(light, 0.5, 0xff9000);
scene.add(lightHelper);
updateStatus(`已添加点光源到场景中`);
}
// 重置所有设置
function resetSettings() {
rotationSpeed = 1.0;
modelScale = 1.0;
wireframeOpacity = 0.5;
vertexSize = 0.05;
// 更新UI元素
document.getElementById('rotation-speed').value = rotationSpeed;
document.getElementById('rotation-value').textContent = rotationSpeed.toFixed(1);
document.getElementById('model-scale').value = modelScale;
document.getElementById('scale-value').textContent = modelScale.toFixed(1);
document.getElementById('wireframe-opacity').value = wireframeOpacity;
document.getElementById('wireframe-value').textContent = wireframeOpacity.toFixed(1);
document.getElementById('vertex-size').value = vertexSize;
document.getElementById('vertex-value').textContent = vertexSize.toFixed(2);
// 重新加载当前模型以应用默认设置
loadModel(getCurrentModelType());
updateStatus("已重置所有设置为默认值");
}
// 初始化UI事件监听
function initUIEvents() {
// 模型类型切换
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const modelType = this.getAttribute('data-model');
loadModel(modelType);
});
});
// 旋转速度控制
const rotationSlider = document.getElementById('rotation-speed');
const rotationValue = document.getElementById('rotation-value');
rotationSlider.addEventListener('input', function() {
rotationSpeed = parseFloat(this.value);
rotationValue.textContent = rotationSpeed.toFixed(1);
});
// 模型尺寸控制
const scaleSlider = document.getElementById('model-scale');
const scaleValue = document.getElementById('scale-value');
scaleSlider.addEventListener('input', function() {
modelScale = parseFloat(this.value);
scaleValue.textContent = modelScale.toFixed(1);
if (modelGroup) {
modelGroup.scale.setScalar(modelScale);
}
});
// 线框透明度控制
const wireframeSlider = document.getElementById('wireframe-opacity');
const wireframeValue = document.getElementById('wireframe-value');
wireframeSlider.addEventListener('input', function() {
wireframeOpacity = parseFloat(this.value);
wireframeValue.textContent = wireframeOpacity.toFixed(1);
if (wireframe) {
wireframe.material.opacity = wireframeOpacity;
wireframe.material.needsUpdate = true;
}
});
// 顶点大小控制
const vertexSlider = document.getElementById('vertex-size');
const vertexValue = document.getElementById('vertex-value');
vertexSlider.addEventListener('input', function() {
vertexSize = parseFloat(this.value);
vertexValue.textContent = vertexSize.toFixed(2);
if (vertices) {
vertices.material.size = vertexSize;
vertices.material.needsUpdate = true;
}
});
// 按钮事件
document.getElementById('reset-btn').addEventListener('click', resetSettings);
document.getElementById('export-btn').addEventListener('click', exportGeometryData);
document.getElementById('add-light').addEventListener('click', addLight);
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
init();
initUIEvents();
});
</script>
</body>
</html>
这个Three.js自定义模型解析系统包含以下功能:
四种预定义模型:球体、立方体、圆环、自定义二十面体
实时3D渲染,支持旋转、缩放和平移
模型、线框和顶点同时显示
实时显示模型的几何数据:顶点数、三角面数
显示模型的边界框尺寸
可导出模型的几何数据为文本文件
模型切换:点击不同模型类型按钮切换显示
旋转速度控制:调整模型自动旋转速度
模型尺寸控制:实时调整模型大小
线框透明度:控制线框显示强度
顶点大小:调整顶点可视化点的大小
添加光源:在场景中添加随机位置的点光源
重置设置:将所有控制恢复到默认值
响应式设计:适配不同屏幕尺寸
使用Three.js r128版本
基于BufferGeometry的高效渲染
使用OrbitControls实现相机控制
实时更新UI反馈
这个演示展示了Three.js如何解析和处理3D模型数据,让用户可以直观地了解模型的几何结构,并实时调整各种参数来观察模型的变化。