Files
WAI_Project_UNIX/static/html/planet_3d.html
2026-01-21 01:37:34 +08:00

311 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>3D Planet Interaction</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: #f8f9fa;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
touch-action: none;
}
canvas { width: 100%; height: 100%; display: block; }
#loader {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: #52c41a; font-size: 14px;
font-weight: 500;
display: flex; flex-direction: column; align-items: center;
z-index: 100;
}
.spinner {
width: 36px; height: 36px; border: 3px solid #e8f5e9;
border-top: 3px solid #52c41a; border-radius: 50%;
animation: spin 0.8s linear infinite; margin-bottom: 12px;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
#error {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: #ff4d4f; font-size: 14px;
text-align: center; display: none;
padding: 20px;
}
.controls-hint {
position: absolute; bottom: 20px; left: 50%;
transform: translateX(-50%);
color: #999; font-size: 12px;
background: rgba(255,255,255,0.9);
padding: 6px 12px; border-radius: 16px;
opacity: 0; transition: opacity 0.3s;
}
.controls-hint.show { opacity: 1; }
</style>
</head>
<body>
<div id="loader">
<div class="spinner"></div>
<span>加载 3D 模型...</span>
</div>
<div id="error">
<div>模型加载失败</div>
<div style="font-size: 12px; margin-top: 8px; color: #999;">请检查网络连接</div>
</div>
<div class="controls-hint" id="hint">双指缩放 · 单指旋转</div>
<!-- Three.js 核心库 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.146.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.146.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.146.0/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.146.0/examples/js/loaders/FBXLoader.js"></script>
<script>
let scene, camera, renderer, controls, model;
let modelUrl = null;
let modelFormat = 'glb';
// 从 URL 参数获取模型信息
const urlParams = new URLSearchParams(window.location.search);
modelUrl = urlParams.get('modelUrl');
modelFormat = urlParams.get('format') || 'glb';
init();
animate();
function init() {
// 场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf8f9fa);
// 相机
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
camera.position.set(0, 100, 300);
// 光照系统
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(200, 200, 100);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
scene.add(directionalLight);
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
fillLight.position.set(-100, 50, -100);
scene.add(fillLight);
const pointLight = new THREE.PointLight(0x52c41a, 0.4);
pointLight.position.set(-100, 50, -50);
scene.add(pointLight);
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// 控制器
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.8;
controls.enablePan = true;
controls.panSpeed = 0.5;
controls.minDistance = 100;
controls.maxDistance = 600;
controls.maxPolarAngle = Math.PI * 0.85;
controls.minPolarAngle = Math.PI * 0.15;
// 交互时停止自动旋转
controls.addEventListener('start', () => {
controls.autoRotate = false;
showHint();
});
// 加载模型
if (modelUrl) {
loadModel(modelUrl, modelFormat);
} else {
createDefaultPlanet();
}
window.addEventListener('resize', onWindowResize);
// 接收来自小程序的消息
window.addEventListener('message', (event) => {
const data = event.data;
if (data && data.type === 'loadModel') {
loadModel(data.url, data.format || 'glb');
}
});
}
function loadModel(url, format) {
showLoader();
let loader;
if (format === 'fbx') {
loader = new THREE.FBXLoader();
} else {
loader = new THREE.GLTFLoader();
}
loader.load(
url,
(result) => {
hideLoader();
// 移除旧模型
if (model) {
scene.remove(model);
}
// 获取模型
model = format === 'fbx' ? result : result.scene;
// 居中和缩放
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 150 / maxDim;
model.position.sub(center);
model.scale.setScalar(scale);
// 启用阴影
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(model);
},
(progress) => {
if (progress.total > 0) {
const percent = Math.round((progress.loaded / progress.total) * 100);
updateLoaderText(`加载中 ${percent}%`);
}
},
(error) => {
console.error('模型加载失败:', error);
hideLoader();
showError();
// 加载失败时显示默认星球
createDefaultPlanet();
}
);
}
function createDefaultPlanet() {
hideLoader();
const group = new THREE.Group();
// 星球本体 - 更精细的几何体
const planetGeom = new THREE.SphereGeometry(80, 64, 64);
const planetMat = new THREE.MeshPhongMaterial({
color: 0x95de64,
shininess: 40,
flatShading: false
});
const planet = new THREE.Mesh(planetGeom, planetMat);
planet.receiveShadow = true;
planet.castShadow = true;
group.add(planet);
// 装饰层 - 网格效果
const decoGeom = new THREE.SphereGeometry(82, 32, 32);
const decoMat = new THREE.MeshPhongMaterial({
color: 0x52c41a,
wireframe: true,
transparent: true,
opacity: 0.08
});
const deco = new THREE.Mesh(decoGeom, decoMat);
group.add(deco);
// 添加一些装饰点
const dotGeom = new THREE.SphereGeometry(3, 16, 16);
const dotMat = new THREE.MeshPhongMaterial({ color: 0x73d13d });
for (let i = 0; i < 20; i++) {
const dot = new THREE.Mesh(dotGeom, dotMat);
const phi = Math.random() * Math.PI * 2;
const theta = Math.random() * Math.PI;
const r = 82;
dot.position.set(
r * Math.sin(theta) * Math.cos(phi),
r * Math.sin(theta) * Math.sin(phi),
r * Math.cos(theta)
);
group.add(dot);
}
model = group;
scene.add(model);
}
function showLoader() {
document.getElementById('loader').style.display = 'flex';
document.getElementById('error').style.display = 'none';
}
function hideLoader() {
document.getElementById('loader').style.display = 'none';
}
function updateLoaderText(text) {
const loader = document.getElementById('loader');
const span = loader.querySelector('span');
if (span) span.textContent = text;
}
function showError() {
document.getElementById('error').style.display = 'block';
setTimeout(() => {
document.getElementById('error').style.display = 'none';
}, 3000);
}
function showHint() {
const hint = document.getElementById('hint');
hint.classList.add('show');
setTimeout(() => {
hint.classList.remove('show');
}, 2000);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
// 暴露给小程序调用的方法
window.loadModel = loadModel;
window.resetView = function() {
camera.position.set(0, 100, 300);
controls.reset();
controls.autoRotate = true;
};
</script>
</body>
</html>