311 lines
11 KiB
HTML
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>
|