<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D 互动场景</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="canvas-container"></div>
<script type="module" src="app.js"></script>
</body>
</html>
index.html
style.css
app.js
package.json
现在支持上传本地图片了!
index.html
style.css
/* 代码由 AI.笔COOL Agent 生成 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background-color: #000;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#canvas-container {
width: 100vw;
height: 100vh;
display: block;
}
/* 简单的加载提示 */
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 1.2rem;
pointer-events: none;
transition: opacity 0.5s ease;
}
body.loaded #loading {
opacity: 0;
}
/* UI 覆盖层样式 */
#ui-layer {
position: absolute;
top: 20px;
left: 20px;
color: #fff;
z-index: 10;
pointer-events: none;
}
h1 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
p {
font-size: 0.9rem;
opacity: 0.8;
max-width: 300px;
}
/* 控制面板样式 */
#controls {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 15px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
z-index: 10;
pointer-events: auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.control-group {
margin-bottom: 10px;
}
.control-group:last-child {
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 5px;
font-size: 0.85rem;
}
input[type="range"] {
width: 150px;
cursor: pointer;
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s;
}
button:hover {
background: #45a049;
}
button.reset {
background: #f44336;
margin-left: 10px;
}
button.reset:hover {
background: #d32f2f;
}
/* 移动端适配 */
@media (max-width: 768px) {
#controls {
bottom: 10px;
right: 10px;
left: 10px;
padding: 10px;
}
input[type="range"] {
width: 100%;
}
h1 {
font-size: 1.2rem;
}
}
编辑器加载中
app.js
/* 代码由 AI.笔COOL Agent 生成 */
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// 场景、相机、渲染器配置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB); // 天空蓝背景
scene.fog = new THREE.Fog(0x87CEEB, 20, 100); // 添加雾效增加深度感
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 15, 30);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('canvas-container').appendChild(renderer.domElement);
// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 - 0.05; // 防止相机低于地面
controls.minDistance = 10;
controls.maxDistance = 50;
// 灯光系统
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(20, 40, 20);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 100;
dirLight.shadow.camera.left = -30;
dirLight.shadow.camera.right = 30;
dirLight.shadow.camera.top = 30;
dirLight.shadow.camera.bottom = -30;
scene.add(dirLight);
// 材质定义
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x4caf50,
roughness: 0.8,
metalness: 0.1
});
const buildingMaterials = [
new THREE.MeshStandardMaterial({ color: 0xff7043, roughness: 0.5 }), // 红砖
new THREE.MeshStandardMaterial({ color: 0x42a5f5, roughness: 0.5 }), // 蓝瓦
new THREE.MeshStandardMaterial({ color: 0xffeb3b, roughness: 0.5 }), // 黄窗
new THREE.MeshStandardMaterial({ color: 0x8d6e63, roughness: 0.6 }) // 木色
];
const treeTrunkMaterial = new THREE.MeshStandardMaterial({ color: 0x795548, roughness: 0.9 });
const treeLeavesMaterial = new THREE.MeshStandardMaterial({ color: 0x2e7d32, roughness: 0.8 });
// 创建地形
const groundGeometry = new THREE.PlaneGeometry(100, 100, 32, 32);
const positionAttribute = groundGeometry.attributes.position;
for (let i = 0; i < positionAttribute.count; i++) {
const x = positionAttribute.getX(i);
const y = positionAttribute.getY(i);
// 简单的噪声模拟地形起伏
const z = Math.sin(x * 0.1) * 0.5 + Math.cos(y * 0.1) * 0.5 + Math.random() * 0.1;
positionAttribute.setZ(i, z);
}
groundGeometry.computeVertexNormals();
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// 创建建筑生成器
function createBuilding(x, z, width, height, depth, colorIndex) {
const group = new THREE.Group();
// 主体
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = buildingMaterials[colorIndex % buildingMaterials.length];
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = height / 2;
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
// 屋顶
const roofHeight = height * 0.2;
const roofGeometry = new THREE.ConeGeometry(Math.max(width, depth) * 0.8, roofHeight, 4);
const roofMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 });
const roof = new THREE.Mesh(roofGeometry, roofMaterial);
roof.position.y = height + roofHeight / 2;
roof.rotation.y = Math.PI / 4;
roof.castShadow = true;
group.add(roof);
// 窗户 (简单模拟)
const windowGeo = new THREE.PlaneGeometry(width * 0.2, height * 0.15);
const windowMat = new THREE.MeshStandardMaterial({
color: 0xffffcc,
emissive: 0xffffcc,
emissiveIntensity: 0.2
});
for (let i = 0; i < 3; i++) {
const window1 = new THREE.Mesh(windowGeo, windowMat);
window1.position.set(0, height * 0.3 + i * height * 0.2, depth / 2 + 0.01);
group.add(window1);
const window2 = new THREE.Mesh(windowGeo, windowMat);
window2.position.set(0, height * 0.3 + i * height * 0.2, -depth / 2 - 0.01);
window2.rotation.y = Math.PI;
group.add(window2);
}
group.position.set(x, 0, z);
scene.add(group);
return group;
}
// 创建树木生成器
function createTree(x, z, scale = 1) {
const group = new THREE.Group();
// 树干
const trunkGeo = new THREE.CylinderGeometry(0.3 * scale, 0.4 * scale, 2 * scale, 8);
const trunk = new THREE.Mesh(trunkGeo, treeTrunkMaterial);
trunk.position.y = 1 * scale;
trunk.castShadow = true;
group.add(trunk);
// 树叶 (多层圆锥)
const leafSizes = [2.5, 2.0, 1.5];
const leafHeights = [3.5, 4.5, 5.5];
leafSizes.forEach((size, i) => {
const leafGeo = new THREE.ConeGeometry(size * scale, 1.5 * scale, 8);
const leaf = new THREE.Mesh(leafGeo, treeLeavesMaterial);
leaf.position.y = leafHeights[i] * scale;
leaf.castShadow = true;
group.add(leaf);
});
group.position.set(x, 0, z);
scene.add(group);
return group;
}
// 随机散布建筑和树木
function random(min, max) {
return Math.random() * (max - min) + min;
}
const buildings = [];
const trees = [];
// 生成市中心区域
for (let i = 0; i < 15; i++) {
const x = random(-15, 15);
const z = random(-15, 15);
const w = random(2, 4);
const h = random(4, 10);
const d = random(2, 4);
buildings.push(createBuilding(x, z, w, h, d, i));
}
// 生成郊区树木
for (let i = 0; i < 30; i++) {
const angle = random(0, Math.PI * 2);
const radius = random(20, 45);
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
trees.push(createTree(x, z, random(0.8, 1.5)));
}
// 生成中心广场装饰
const centerPlazaGeo = new THREE.CylinderGeometry(5, 5, 0.1, 32);
const centerPlazaMat = new THREE.MeshStandardMaterial({ color: 0xcccccc });
const centerPlaza = new THREE.Mesh(centerPlazaGeo, centerPlazaMat);
centerPlaza.position.y = 0.1;
centerPlaza.receiveShadow = true;
scene.add(centerPlaza);
// 交互控制逻辑
let speed = 1;
let rotationSpeed = 1;
const speedSlider = document.getElementById('speed-slider');
const rotSlider = document.getElementById('rot-slider');
if (speedSlider) {
speedSlider.addEventListener('input', (e) => {
speed = parseFloat(e.target.value);
});
}
if (rotSlider) {
rotSlider.addEventListener('input', (e) => {
rotationSpeed = parseFloat(e.target.value);
});
}
// 动画循环
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
// 缓慢旋转整个场景或调整视角(如果启用了自动旋转)
// 这里我们让相机稍微环绕,增加动态感
camera.position.x = Math.sin(time * 0.1 * rotationSpeed) * 30;
camera.position.z = Math.cos(time * 0.1 * rotationSpeed) * 30;
camera.lookAt(0, 5, 0);
controls.update();
renderer.render(scene, camera);
}
// 窗口大小调整处理
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 启动
animate();
// 添加UI控制逻辑(如果存在)
const resetBtn = document.getElementById('reset-camera');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
camera.position.set(0, 15, 30);
camera.lookAt(0, 0, 0);
controls.reset();
});
}
// 添加场景说明文本
const uiLayer = document.getElementById('ui-layer');
if (uiLayer) {
uiLayer.innerHTML = `
<h1>3D 互动场景</h1>
<p>拖动旋转视角,滚轮缩放。场景包含随机生成的建筑与树木,带有动态光影效果。</p>
`;
}
// 添加控制面板
const controlsDiv = document.getElementById('controls');
if (controlsDiv) {
controlsDiv.innerHTML = `
<div class="control-group">
<label for="speed-slider">漫游速度</label>
<input type="range" id="speed-slider" min="0.1" max="2" step="0.1" value="1">
</div>
<div class="control-group">
<label for="rot-slider">旋转速度</label>
<input type="range" id="rot-slider" min="0.1" max="2" step="0.1" value="1">
</div>
<div class="control-group">
<button id="reset-camera">重置视角</button>
</div>
`;
}
编辑器加载中
package.json
注意:新添加的依赖包首次加载可能会报错,稍后再次刷新即可
{
"dependencies": {
"three": "0.173.0"
}
}
编辑器加载中
预览页面