403 lines
15 KiB
HTML
403 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>3D Gallery - Center View & Realistic Materials</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
canvas {
|
|
display: block;
|
|
}
|
|
|
|
#info {
|
|
position: absolute;
|
|
top: 10px;
|
|
width: 100%;
|
|
text-align: center;
|
|
color: white;
|
|
pointer-events: none;
|
|
text-shadow: 1px 1px 1px black;
|
|
}
|
|
|
|
#loading {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-size: 24px;
|
|
text-shadow: 1px 1px 1px black;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="info">3D Gallery - 拖动鼠标旋转视图,滚轮缩放</div>
|
|
<div id="loading">Loading...</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/TextureLoader.js"></script>
|
|
<script>
|
|
// 初始化场景、相机和渲染器
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x111111);
|
|
|
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.set(0, 1.6, 0); // 1.6米高,模拟人眼高度
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// 场景中心点(观察目标)
|
|
const centerPoint = new THREE.Vector3(0, 1.6, 0);
|
|
|
|
// 添加轨道控制器,设置目标为场景中心
|
|
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
controls.target.copy(centerPoint);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.minDistance = 1;
|
|
controls.maxDistance = 10;
|
|
controls.maxPolarAngle = Math.PI * 0.8; // 限制不能看穿地面
|
|
|
|
// 添加灯光
|
|
const ambientLight = new THREE.AmbientLight(0x404040);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
directionalLight.position.set(1, 2, 1);
|
|
scene.add(directionalLight);
|
|
|
|
const pointLight = new THREE.PointLight(0xffffff, 0.5, 10);
|
|
pointLight.position.set(0, 3, 0);
|
|
scene.add(pointLight);
|
|
|
|
// 创建画廊展品
|
|
const galleryItems = [];
|
|
const itemCount = 8;
|
|
const radius = 4; // 缩小半径使画作更集中
|
|
|
|
// 创建纹理加载器
|
|
const textureLoader = new THREE.TextureLoader();
|
|
|
|
// 画框材质
|
|
const frameMaterials = [
|
|
new THREE.MeshStandardMaterial({ color: 0xc9b38b, metalness: 0.3, roughness: 0.4 }),
|
|
new THREE.MeshStandardMaterial({ color: 0x7a6a5f, metalness: 0.4, roughness: 0.6 }),
|
|
new THREE.MeshStandardMaterial({ color: 0xe5e5e5, metalness: 0.7, roughness: 0.2 }),
|
|
new THREE.MeshStandardMaterial({ color: 0x3a2e1e, metalness: 0.1, roughness: 0.8 })
|
|
];
|
|
|
|
// 创建带画框的照片(自适应图片比例)
|
|
function createFramedPhoto(texture, frameMaterial, maxWidth, maxHeight, frameWidth) {
|
|
const group = new THREE.Group();
|
|
|
|
// 计算图片实际比例
|
|
const imageRatio = texture.image.width / texture.image.height;
|
|
let width, height;
|
|
|
|
// 根据图片比例调整显示尺寸
|
|
if (imageRatio > 1) {
|
|
// 横向图片
|
|
width = maxWidth;
|
|
height = maxWidth / imageRatio;
|
|
} else {
|
|
// 纵向图片
|
|
height = maxHeight;
|
|
width = maxHeight * imageRatio;
|
|
}
|
|
|
|
// 照片部分
|
|
const photoGeometry = new THREE.PlaneGeometry(width, height);
|
|
const photoMaterial = new THREE.MeshStandardMaterial({
|
|
map: texture,
|
|
side: THREE.DoubleSide,
|
|
|
|
});
|
|
const photo = new THREE.Mesh(photoGeometry, photoMaterial);
|
|
group.add(photo);
|
|
|
|
// 画框部分(根据实际图片尺寸调整)
|
|
const frameShape = new THREE.Shape();
|
|
const outerWidth = width + frameWidth * 2;
|
|
const outerHeight = height + frameWidth * 2;
|
|
|
|
frameShape.moveTo(-outerWidth / 2, -outerHeight / 2);
|
|
frameShape.lineTo(outerWidth / 2, -outerHeight / 2);
|
|
frameShape.lineTo(outerWidth / 2, outerHeight / 2);
|
|
frameShape.lineTo(-outerWidth / 2, outerHeight / 2);
|
|
frameShape.lineTo(-outerWidth / 2, -outerHeight / 2);
|
|
|
|
const innerPath = new THREE.Path();
|
|
innerPath.moveTo(-width / 2, -height / 2);
|
|
innerPath.lineTo(width / 2, -height / 2);
|
|
innerPath.lineTo(width / 2, height / 2);
|
|
innerPath.lineTo(-width / 2, height / 2);
|
|
innerPath.lineTo(-width / 2, -height / 2);
|
|
|
|
frameShape.holes.push(innerPath);
|
|
|
|
const extrudeSettings = {
|
|
steps: 1,
|
|
depth: frameWidth / 2,
|
|
bevelEnabled: true,
|
|
bevelThickness: 0.05,
|
|
bevelSize: 0.05,
|
|
bevelOffset: 0,
|
|
bevelSegments: 8
|
|
};
|
|
|
|
const frameGeometry = new THREE.ExtrudeGeometry(frameShape, extrudeSettings);
|
|
const frame = new THREE.Mesh(frameGeometry, frameMaterial);
|
|
frame.position.z = -frameWidth / 4;
|
|
group.add(frame);
|
|
|
|
// 添加画框背面
|
|
const backGeometry = new THREE.PlaneGeometry(outerWidth, outerHeight);
|
|
const backMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x222222,
|
|
side: THREE.DoubleSide
|
|
});
|
|
const back = new THREE.Mesh(backGeometry, backMaterial);
|
|
back.position.z = -frameWidth / 2;
|
|
group.add(back);
|
|
|
|
return group;
|
|
}
|
|
|
|
|
|
// 加载纹理
|
|
const textures = [];
|
|
let loadedTextures = 0;
|
|
|
|
// 加载地面和墙壁纹理
|
|
const floorTexture = textureLoader.load('https://threejs.org/examples/textures/hardwood2_diffuse.jpg');
|
|
const floorNormalMap = textureLoader.load('https://threejs.org/examples/textures/hardwood2_normal.jpg');
|
|
const floorRoughnessMap = textureLoader.load('https://threejs.org/examples/textures/hardwood2_roughness.jpg');
|
|
|
|
const wallTexture = textureLoader.load('https://threejs.org/examples/textures/brick_diffuse.jpg');
|
|
const wallNormalMap = textureLoader.load('https://threejs.org/examples/textures/brick_normal.jpg');
|
|
const wallRoughnessMap = textureLoader.load('https://threejs.org/examples/textures/brick_roughness.jpg');
|
|
|
|
function loadTextures() {
|
|
// 加载画作纹理
|
|
for (let i = 0; i < itemCount; i++) {
|
|
textureLoader.load(
|
|
`./${i + 1}.jpg`,
|
|
(texture) => {
|
|
textures[i] = texture;
|
|
loadedTextures++;
|
|
|
|
if (loadedTextures === itemCount) {
|
|
initGallery();
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
},
|
|
undefined,
|
|
(err) => {
|
|
console.error('Error loading texture', err);
|
|
textures[i] = null;
|
|
loadedTextures++;
|
|
|
|
if (loadedTextures === itemCount) {
|
|
initGallery();
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
function initGallery() {
|
|
// 创建展品
|
|
for (let i = 0; i < itemCount; i++) {
|
|
const angle = (i / itemCount) * Math.PI * 2;
|
|
const x = Math.cos(angle) * radius;
|
|
const z = Math.sin(angle) * radius;
|
|
|
|
const frameMaterial = frameMaterials[Math.floor(Math.random() * frameMaterials.length)];
|
|
|
|
const framedPhoto = createFramedPhoto(
|
|
textures[i], // 加载的纹理
|
|
frameMaterial, // 画框材质
|
|
2.0, // 最大宽度
|
|
1.5, // 最大高度
|
|
0.05 // 画框宽度
|
|
);
|
|
|
|
framedPhoto.position.set(x, 1.6, z); // 画作中心与眼睛平齐
|
|
framedPhoto.lookAt(centerPoint);
|
|
|
|
// 为整个group添加用户数据
|
|
framedPhoto.userData = {
|
|
id: i,
|
|
title: `Artwork ${i + 1}`,
|
|
description: `This is a description for artwork ${i + 1}`,
|
|
originalPosition: new THREE.Vector3(x, 1.6, z),
|
|
originalRotation: framedPhoto.rotation.clone(),
|
|
originalScale: new THREE.Vector3(1, 1, 1)
|
|
};
|
|
|
|
// 为group中的每个子对象添加引用
|
|
framedPhoto.children.forEach(child => {
|
|
child.userData.parentGroup = framedPhoto;
|
|
});
|
|
|
|
scene.add(framedPhoto);
|
|
galleryItems.push(framedPhoto);
|
|
}
|
|
|
|
// 添加地面 - 使用真实木质纹理
|
|
const floorGeometry = new THREE.CircleGeometry(radius + 2, 64);
|
|
const floorMaterial = new THREE.MeshStandardMaterial({
|
|
map: floorTexture,
|
|
normalMap: floorNormalMap,
|
|
roughnessMap: floorRoughnessMap,
|
|
roughness: 0.8,
|
|
metalness: 0.1,
|
|
side: THREE.DoubleSide
|
|
});
|
|
|
|
// 调整纹理重复
|
|
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
|
|
floorTexture.repeat.set(4, 4);
|
|
floorNormalMap.wrapS = floorNormalMap.wrapT = THREE.RepeatWrapping;
|
|
floorNormalMap.repeat.set(4, 4);
|
|
floorRoughnessMap.wrapS = floorRoughnessMap.wrapT = THREE.RepeatWrapping;
|
|
floorRoughnessMap.repeat.set(4, 4);
|
|
|
|
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
|
floor.rotation.x = -Math.PI / 2;
|
|
floor.position.y = 0;
|
|
scene.add(floor);
|
|
|
|
// 添加墙壁 - 使用真实砖墙纹理
|
|
const wallHeight = 3.5;
|
|
const wallGeometry = new THREE.CylinderGeometry(radius + 2, radius + 2, wallHeight, 64, 1, true);
|
|
const wallMaterial = new THREE.MeshStandardMaterial({
|
|
map: wallTexture,
|
|
normalMap: wallNormalMap,
|
|
roughnessMap: wallRoughnessMap,
|
|
roughness: 0.7,
|
|
metalness: 0,
|
|
side: THREE.DoubleSide
|
|
});
|
|
|
|
// 调整纹理重复
|
|
wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping;
|
|
wallTexture.repeat.set(2, 1);
|
|
wallNormalMap.wrapS = wallNormalMap.wrapT = THREE.RepeatWrapping;
|
|
wallNormalMap.repeat.set(2, 1);
|
|
wallRoughnessMap.wrapS = wallRoughnessMap.wrapT = THREE.RepeatWrapping;
|
|
wallRoughnessMap.repeat.set(2, 1);
|
|
|
|
const walls = new THREE.Mesh(wallGeometry, wallMaterial);
|
|
walls.position.y = wallHeight / 2;
|
|
scene.add(walls);
|
|
|
|
// 添加聚光灯照射每幅画
|
|
for (let i = 0; i < itemCount; i++) {
|
|
const item = galleryItems[i];
|
|
const spotLight = new THREE.SpotLight(0xffffff, 0.8, 10, Math.PI / 6, 0.5);
|
|
spotLight.position.set(
|
|
item.userData.originalPosition.x * 1.5,
|
|
item.userData.originalPosition.y + 1.5,
|
|
item.userData.originalPosition.z * 1.5
|
|
);
|
|
spotLight.target.position.copy(item.position);
|
|
scene.add(spotLight);
|
|
scene.add(spotLight.target);
|
|
|
|
// 添加灯光辅助(调试用)
|
|
// const helper = new THREE.SpotLightHelper(spotLight);
|
|
// scene.add(helper);
|
|
}
|
|
}
|
|
|
|
// 加载纹理
|
|
loadTextures();
|
|
|
|
// 交互系统
|
|
const raycaster = new THREE.Raycaster();
|
|
const mouse = new THREE.Vector2();
|
|
let selectedItem = null;
|
|
|
|
// 鼠标移动事件处理
|
|
function onMouseMove(event) {
|
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
raycaster.setFromCamera(mouse, camera);
|
|
|
|
const intersects = raycaster.intersectObjects(scene.children, true);
|
|
|
|
if (selectedItem) {
|
|
selectedItem.position.copy(selectedItem.userData.originalPosition);
|
|
selectedItem.rotation.copy(selectedItem.userData.originalRotation);
|
|
selectedItem.scale.copy(selectedItem.userData.originalScale);
|
|
selectedItem = null;
|
|
}
|
|
|
|
for (let i = 0; i < intersects.length; i++) {
|
|
const intersected = intersects[i].object;
|
|
|
|
if (intersected.userData.parentGroup) {
|
|
const parentGroup = intersected.userData.parentGroup;
|
|
|
|
parentGroup.scale.set(1.1, 1.1, 1.1);
|
|
const direction = new THREE.Vector3().subVectors(
|
|
parentGroup.position,
|
|
centerPoint
|
|
).normalize();
|
|
parentGroup.position.add(direction.multiplyScalar(0.3));
|
|
parentGroup.lookAt(centerPoint);
|
|
|
|
selectedItem = parentGroup;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function onClick() {
|
|
if (selectedItem) {
|
|
console.log('Selected:', selectedItem.userData);
|
|
alert(`Selected: ${selectedItem.userData.title}\n${selectedItem.userData.description}`);
|
|
}
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
window.addEventListener('resize', onWindowResize);
|
|
window.addEventListener('mousemove', onMouseMove, false);
|
|
// window.addEventListener('click', onClick, false);
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
controls.update();
|
|
|
|
if (!controls.target.equals(centerPoint)) {
|
|
controls.target.copy(centerPoint);
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
animate();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |