鱼缸edit icon

作者:
lynn-ssk
Fork(复制)
下载
嵌入
BUG反馈
index.html
现在支持上传本地图片了!
index.html
            
            <!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>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { overflow: hidden; background: #1a1a2e; font-family: 'Segoe UI', sans-serif; }
  canvas { display: block; }
  #info {
    position: fixed; top: 15px; left: 15px;
    color: #e0f7ff; font-size: 13px;
    background: rgba(0,20,40,0.7); padding: 12px 18px;
    border-radius: 10px; border: 1px solid rgba(0,150,255,0.3);
    backdrop-filter: blur(5px); pointer-events: none; z-index: 10;
  }
  #info h3 { margin-bottom: 6px; color: #4fc3f7; font-size: 15px; }
  #info p { line-height: 1.5; opacity: 0.85; }
</style>
</head>
<body>
<div id="info">
  <h3>🐟 3D 鱼群模拟</h3>
  <p>10条鱼遵循 Boids Plus 规则<br>水草随水流动态摆动</p>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(() => {
  // ==================== 场景初始化 ====================
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x1a1a2e);
  scene.fog = new THREE.Fog(0x1a1a2e, 15, 30);

  const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
  camera.position.set(0, 3, 8);
  camera.lookAt(0, 0.5, 0);

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(innerWidth, innerHeight);
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.2;
  document.body.appendChild(renderer.domElement);

  // ==================== 灯光 ====================
  const ambientLight = new THREE.AmbientLight(0x404060, 0.6);
  scene.add(ambientLight);

  const mainLight = new THREE.DirectionalLight(0xffeedd, 1.2);
  mainLight.position.set(5, 10, 5);
  mainLight.castShadow = true;
  mainLight.shadow.mapSize.set(2048, 2048);
  mainLight.shadow.camera.near = 0.5;
  mainLight.shadow.camera.far = 30;
  mainLight.shadow.camera.left = -8;
  mainLight.shadow.camera.right = 8;
  mainLight.shadow.camera.top = 8;
  mainLight.shadow.camera.bottom = -8;
  scene.add(mainLight);

  const fillLight = new THREE.DirectionalLight(0x4488ff, 0.4);
  fillLight.position.set(-5, 3, -3);
  scene.add(fillLight);

  const waterLight = new THREE.PointLight(0x00aaff, 1.5, 10);
  waterLight.position.set(0, 2, 0);
  scene.add(waterLight);

  const pointLight2 = new THREE.PointLight(0xff8844, 0.5, 12);
  pointLight2.position.set(-4, 4, 3);
  scene.add(pointLight2);

  // ==================== 地面 ====================
  const floorGeo = new THREE.PlaneGeometry(30, 30);
  const floorMat = new THREE.MeshStandardMaterial({ color: 0x2a2a3e, roughness: 0.9 });
  const floor = new THREE.Mesh(floorGeo, floorMat);
  floor.rotation.x = -Math.PI / 2;
  floor.position.y = -0.5;
  floor.receiveShadow = true;
  scene.add(floor);

  // ==================== 桌子 ====================
  const tableGroup = new THREE.Group();
  const tableMat = new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.6, metalness: 0.1 });

  // 桌面
  const tableTop = new THREE.Mesh(new THREE.BoxGeometry(5, 0.15, 3.5), tableMat);
  tableTop.position.y = 0.075;
  tableTop.castShadow = true;
  tableTop.receiveShadow = true;
  tableGroup.add(tableTop);

  // 桌腿
  const legPositions = [[-2.2, -0.4, -1.5], [2.2, -0.4, -1.5], [-2.2, -0.4, 1.5], [2.2, -0.4, 1.5]];
  legPositions.forEach(p => {
    const leg = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.9, 0.12), tableMat);
    leg.position.set(p[0], p[1], p[2]);
    leg.castShadow = true;
    tableGroup.add(leg);
  });

  // 横梁
  const beamMat = new THREE.MeshStandardMaterial({ color: 0x7A5230, roughness: 0.7 });
  [[-2.2, 0, 0], [2.2, 0, 0]].forEach(p => {
    const beam = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.08, 3), beamMat);
    beam.position.set(p[0], p[1], p[2]);
    tableGroup.add(beam);
  });

  scene.add(tableGroup);

  // ==================== 鱼缸 ====================
  const TANK_W = 3.2, TANK_H = 2.2, TANK_D = 1.8;
  const TANK_Y = 0.15;

  // 玻璃材质
  const glassMat = new THREE.MeshPhysicalMaterial({
    color: 0x88ccff, transparent: true, opacity: 0.15,
    roughness: 0.05, metalness: 0.1, side: THREE.DoubleSide,
    transmission: 0.9
  });

  const tankGroup = new THREE.Group();
  tankGroup.position.y = TANK_Y;

  // 玻璃壁
  const wallMat = glassMat.clone();
  const walls = [
    { s: [TANK_W, TANK_H, 0.05], p: [0, TANK_H/2, TANK_D/2] },
    { s: [TANK_W, TANK_H, 0.05], p: [0, TANK_H/2, -TANK_D/2] },
    { s: [0.05, TANK_H, TANK_D], p: [TANK_W/2, TANK_H/2, 0] },
    { s: [0.05, TANK_H, TANK_D], p: [-TANK_W/2, TANK_H/2, 0] },
  ];
  walls.forEach(w => {
    const wall = new THREE.Mesh(new THREE.BoxGeometry(...w.s), wallMat);
    wall.position.set(...w.p);
    tankGroup.add(wall);
  });

  // 底部
  const bottomMat = new THREE.MeshStandardMaterial({ color: 0x5c3a1e, roughness: 0.9 });
  const bottom = new THREE.Mesh(new THREE.BoxGeometry(TANK_W, 0.05, TANK_D), bottomMat);
  bottom.position.y = 0.025;
  bottom.receiveShadow = true;
  tankGroup.add(bottom);

  // 沙子层
  const sandMat = new THREE.MeshStandardMaterial({ color: 0xd4b896, roughness: 0.95 });
  const sand = new THREE.Mesh(new THREE.BoxGeometry(TANK_W - 0.1, 0.08, TANK_D - 0.1), sandMat);
  sand.position.y = 0.09;
  tankGroup.add(sand);

  // 水面
  const waterGeo = new THREE.PlaneGeometry(TANK_W - 0.15, TANK_D - 0.15, 32, 32);
  const waterMat = new THREE.MeshPhysicalMaterial({
    color: 0x006994, transparent: true, opacity: 0.4,
    roughness: 0.2, metalness: 0.1, side: THREE.DoubleSide
  });
  const water = new THREE.Mesh(waterGeo, waterMat);
  water.rotation.x = -Math.PI / 2;
  water.position.y = TANK_H - 0.15;
  tankGroup.add(water);

  // 缸沿
  const rimMat = new THREE.MeshStandardMaterial({ color: 0x334455, roughness: 0.3, metalness: 0.5 });
  const rimGeo = new THREE.BoxGeometry(TANK_W + 0.1, 0.06, TANK_D + 0.1);
  const rim = new THREE.Mesh(rimGeo, rimMat);
  rim.position.y = TANK_H;
  rim.castShadow = true;
  tankGroup.add(rim);

  scene.add(tankGroup);

  // ==================== 装饰石头 ====================
  const stoneMat = new THREE.MeshStandardMaterial({ color: 0x666677, roughness: 0.8 });
  [[-1.2, 0.12, -0.5, 0.25], [1.0, 0.15, 0.6, 0.3], [0.3, 0.1, -0.7, 0.18]].forEach(([x,y,z,r]) => {
    const stone = new THREE.Mesh(new THREE.SphereGeometry(r, 8, 6), stoneMat);
    stone.position.set(x, TANK_Y + y, z);
    stone.scale.y = 0.5;
    stone.castShadow = true;
    scene.add(stone);
  });

  // ==================== 鱼模型创建 ====================
  function createFishMesh(color) {
    const group = new THREE.Group();

    // 身体 - 流线型
    const bodyGeo = new THREE.CylinderGeometry(0.04, 0.07, 0.35, 8, 4);
    bodyGeo.rotateZ(Math.PI / 2);
    // 压扁一点
    bodyGeo.scale(1, 0.65, 1.3);
    const bodyMat = new THREE.MeshStandardMaterial({
      color: color, roughness: 0.3, metalness: 0.4,
      emissive: color, emissiveIntensity: 0.15
    });
    const body = new THREE.Mesh(bodyGeo, bodyMat);
    group.add(body);

    // 头部
    const headGeo = new THREE.SphereGeometry(0.055, 8, 6);
    headGeo.scale(1.4, 0.7, 1.2);
    const head = new THREE.Mesh(headGeo, bodyMat);
    head.position.x = 0.18;
    group.add(head);

    // 尾巴
    const tailGeo = new THREE.ConeGeometry(0.1, 0.18, 4);
    tailGeo.rotateZ(-Math.PI / 2);
    const tailMat = new THREE.MeshStandardMaterial({
      color: color, roughness: 0.4, metalness: 0.2,
      emissive: color, emissiveIntensity: 0.1, transparent: true, opacity: 0.85
    });
    const tail = new THREE.Mesh(tailGeo, tailMat);
    tail.position.x = -0.22;
    tail.name = 'tail';
    group.add(tail);

    // 背鳍
    const finGeo = new THREE.ConeGeometry(0.06, 0.12, 3);
    const finMat = new THREE.MeshStandardMaterial({
      color: color, transparent: true, opacity: 0.7, roughness: 0.5
    });
    const backFin = new THREE.Mesh(finGeo, finMat);
    backFin.position.set(0, 0.07, 0);
    backFin.name = 'backFin';
    group.add(backFin);

    // 眼睛
    const eyeGeo = new THREE.SphereGeometry(0.018, 8, 6);
    const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
    const eyePupil = new THREE.MeshBasicMaterial({ color: 0x111111 });

    [[0.22, 0.02, 0.045], [0.22, 0.02, -0.045]].forEach(p => {
      const eye = new THREE.Mesh(eyeGeo, eyeMat);
      eye.position.set(...p);
      group.add(eye);
      const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.01, 6, 4), eyePupil);
      pupil.position.set(p[0] + 0.01, p[1], p[2]);
      group.add(pupil);
    });

    group.castShadow = true;
    return group;
  }

  // ==================== 水草 ====================
  // 在外面创建共享材质(只创建一次)
const seaweedLeafMat = new THREE.MeshStandardMaterial({
  color: 0x2d8a4e, roughness: 0.7, side: THREE.DoubleSide
});

// 优化后的 createSeaweed 函数
function createSeaweed(x, z, height, segments) {
  const group = new THREE.Group();
  group.position.set(x, 0, z);

  const segmentHeight = height / segments;
  const segmentGeo = new THREE.CylinderGeometry(0.02, 0.025, segmentHeight, 5, 1);
  segmentGeo.translate(0, segmentHeight / 2, 0);

  const leaves = [];
  const basePositions = [];
  let currentY = 0;

  // 创建茎干
  for (let i = 0; i < segments; i++) {
    const leaf = new THREE.Mesh(segmentGeo.clone(), seaweedLeafMat);
    leaf.position.set(0, TANK_Y + currentY + 0.1, 0);
    leaf.castShadow = true;
    group.add(leaf);
    leaves.push(leaf);
    basePositions.push(leaf.position.clone());
    currentY += segmentHeight;
  }

  // 添加叶片(减少数量)
  for (let i = 2; i < leaves.length; i += 2) { // 每隔2段加一个叶片
    const leafBlade = new THREE.Mesh(
      new THREE.PlaneGeometry(0.06, segmentHeight * 1.5),
      seaweedLeafMat
    );
    leafBlade.position.set(0, TANK_Y + (i * segmentHeight) + 0.1, 0);
    leafBlade.rotation.y = Math.random() * Math.PI;
    leafBlade.name = 'leafBlade';
    group.add(leafBlade);
    leaves.push(leafBlade);
    basePositions.push(leafBlade.position.clone());
  }

  // 预分配向量对象,避免每帧创建
  group.userData = { 
    leaves, 
    basePositions, 
    segments,
    velocities: leaves.map(() => new THREE.Vector3()), // 预分配速度向量
    tempForce: new THREE.Vector3() // 临时力向量
  };
  
  return group;
}

// 对应的优化后的 updateSeaweeds 函数
function updateSeaweeds() {
  const time = Date.now() * 0.001;
  
  seaweeds.forEach(sw => {
    const { leaves, basePositions, velocities } = sw.userData;

    leaves.forEach((leaf, i) => {
      const velocity = velocities[i];
      const basePos = basePositions[i];
      
      // 计算鱼产生的水流力(简化)
      let forceStrength = 0;
      fishSchool.forEach(fish => {
        const dist = fish.mesh.position.distanceTo(leaf.position);
        if (dist < 1.0) {
          forceStrength += (1.0 - dist) * fish.velocity.length() * 0.5;
        }
      });

      // 弹簧回复力
      const springForce = basePos.clone().sub(leaf.position).multiplyScalar(0.05);
      
      // 自然摆动(简化)
      const swayX = Math.sin(time * 0.6 + i * 0.4) * 0.0005 * (i / leaves.length);
      const swayZ = Math.cos(time * 0.5 + i * 0.3) * 0.0003 * (i / leaves.length);

      // 应用力
      velocity.add(springForce);
      velocity.x += swayX + (forceStrength * 0.01);
      velocity.z += swayZ;
      velocity.multiplyScalar(0.90); // 阻尼

      leaf.position.add(velocity);

      // 限制偏移
      const offset = leaf.position.clone().sub(basePos);
      const maxOffset = 0.1 * (i / leaves.length);
      if (offset.length() > maxOffset) {
        offset.normalize().multiplyScalar(maxOffset);
        leaf.position.copy(basePos).add(offset);
      }
    });
  });
}
  // ==================== 初始化水草 ====================
  const seaweeds = [];
  const plantConfigs = [
    [-1.2, -0.6, 0.8, 12], [-0.8, 0.5, 0.6, 10], [1.0, -0.4, 0.9, 14],
    [1.3, 0.6, 0.5, 8], [-0.3, -0.7, 0.7, 11], [0.5, 0.7, 0.65, 9],
    [-1.4, 0.2, 0.55, 9], [0.0, -0.5, 0.75, 12]
  ];

  plantConfigs.forEach(([x, z, h, s]) => {
    const sw = createSeaweed(x, z, h, s);
    scene.add(sw);
    seaweeds.push(sw);
  });

  // ==================== Boids 鱼群 ====================
  const FISH_COLORS = [
    0xff6b35, 0xffaa22, 0x44aaff, 0xff4488, 0x88ff44,
    0xaa44ff, 0x44ffaa, 0xff8844, 0x4488ff, 0xff44aa
  ];

  class BoidFish {
    constructor(index) {
      this.mesh = createFishMesh(FISH_COLORS[index % FISH_COLORS.length]);
      this.mesh.position.set(
        (Math.random() - 0.5) * (TANK_W * 0.5),
        TANK_Y + 0.3 + Math.random() * (TANK_H * 0.6),
        (Math.random() - 0.5) * (TANK_D * 0.5)
      );
      this.velocity = new THREE.Vector3(
        (Math.random() - 0.5) * 0.02,
        (Math.random() - 0.5) * 0.01,
        (Math.random() - 0.5) * 0.02
      );
      this.acceleration = new THREE.Vector3();
      this.index = index;
      this.tailPhase = Math.random() * Math.PI * 2;
      this.maxSpeed = 0.02 + Math.random() * 0.005;  // 降低速度
      this.tailSpeed = 3 + Math.random() * 2;

      scene.add(this.mesh);
    }

    applyForce(force) {
      this.acceleration.add(force);
    }

    // Boids Plus 规则
    flock(boids) {
      const perceptionRadius = 0.8;
      const separationRadius = 0.5;
      let separation = new THREE.Vector3();
      let alignment = new THREE.Vector3();
      let cohesion = new THREE.Vector3();
      let sepCount = 0, alignCount = 0, cohCount = 0;

      boids.forEach(boid => {
        if (boid === this) return;
        const dist = this.mesh.position.distanceTo(boid.mesh.position);

        if (dist < perceptionRadius) {
          // 分离
          if (dist < separationRadius) {
            const diff = new THREE.Vector3().subVectors(this.mesh.position, boid.mesh.position);
            diff.normalize().divideScalar(dist);
            separation.add(diff);
            sepCount++;
          }
          // 对齐
          alignment.add(boid.velocity);
          alignCount++;
          // 凝聚
          cohesion.add(boid.mesh.position);
          cohCount++;
        }
      });

      if (sepCount > 0) separation.divideScalar(sepCount);
      if (alignCount > 0) {
        alignment.divideScalar(alignCount).normalize().multiplyScalar(this.maxSpeed);
        alignment.sub(this.velocity);
      }
      if (cohCount > 0) {
        cohesion.divideScalar(cohCount);
        cohesion.sub(this.mesh.position).normalize().multiplyScalar(this.maxSpeed);
        cohesion.sub(this.velocity);
      }

      // separation.multiplyScalar(2.5);
      // alignment.multiplyScalar(1.0);
      // cohesion.multiplyScalar(0.8);

      
      separation.multiplyScalar(20);    // 分离力加倍
      alignment.multiplyScalar(0.01);     // 对齐力减半
      cohesion.multiplyScalar(0.1);      // 凝聚力大幅降低

      this.applyForce(separation);
      this.applyForce(alignment);
      this.applyForce(cohesion);
    }

    // 边界避让
    avoidBounds() {
      const margin = 0.3;
      const turnForce = 0.003;
      const halfW = TANK_W / 2 - margin;
      const halfD = TANK_D / 2 - margin;
      const minY = TANK_Y + 0.2;
      const maxY = TANK_Y + TANK_H - 0.3;

      const pos = this.mesh.position;
      const force = new THREE.Vector3();

      if (pos.x > halfW) force.x -= turnForce * (pos.x - halfW) * 5;
      if (pos.x < -halfW) force.x += turnForce * (-halfW - pos.x) * 5;
      if (pos.y > maxY) force.y -= turnForce * (pos.y - maxY) * 5;
      if (pos.y < minY) force.y += turnForce * (minY - pos.y) * 5;
      if (pos.z > halfD) force.z -= turnForce * (pos.z - halfD) * 5;
      if (pos.z < -halfD) force.z += turnForce * (-halfD - pos.z) * 5;

      this.applyForce(force);
    }

    // 水草避让
    avoidPlants() {
      const force = new THREE.Vector3();
      seaweeds.forEach(sw => {
        sw.userData.leaves.forEach(leaf => {
          const dist = this.mesh.position.distanceTo(leaf.position);
          if (dist < 0.2) {
            const dir = new THREE.Vector3().subVectors(this.mesh.position, leaf.position);
            dir.normalize().divideScalar(dist);
            force.add(dir.multiplyScalar(0.005));
          }
        });
      });
      this.applyForce(force);
    }

    update() {
      this.velocity.add(this.acceleration);
      this.velocity.clampLength(0.005, this.maxSpeed);
      this.mesh.position.add(this.velocity);

      // 朝向速度方向
      if (this.velocity.length() > 0.001) {
        const lookTarget = this.mesh.position.clone().add(this.velocity);
        this.mesh.lookAt(lookTarget);
      }

      // 尾巴摆动
      const tail = this.mesh.getObjectByName('tail');
      const backFin = this.mesh.getObjectByName('backFin');
      if (tail) {
        this.tailPhase += this.tailSpeed * 0.05;
        tail.rotation.y = Math.sin(this.tailPhase) * 0.4;
      }
      if (backFin) {
        backFin.rotation.z = Math.sin(this.tailPhase * 0.8) * 0.15;
      }

      this.acceleration.set(0, 0, 0);
    }
  }

  // 初始化鱼群
  const fishSchool = [];
  for (let i = 0; i < 10; i++) {
    fishSchool.push(new BoidFish(i));
  }

  // ==================== 气泡 ====================
  const bubbles = [];
  const bubbleGeo = new THREE.SphereGeometry(0.02, 6, 4);
  const bubbleMat = new THREE.MeshPhysicalMaterial({
    color: 0xffffff, transparent: true, opacity: 0.5, roughness: 0, metalness: 0.1
  });

  for (let i = 0; i < 15; i++) {
    const bubble = new THREE.Mesh(bubbleGeo, bubbleMat.clone());
    bubble.position.set(
      (Math.random() - 0.5) * TANK_W * 0.8,
      TANK_Y + Math.random() * TANK_H,
      (Math.random() - 0.5) * TANK_D * 0.8
    );
    bubble.userData.speed = 0.005 + Math.random() * 0.01;
    bubble.userData.phase = Math.random() * Math.PI * 2;
    scene.add(bubble);
    bubbles.push(bubble);
  }

  // ==================== 水面波浪 ====================
  function updateWater(time) {
    const positions = waterGeo.attributes.position;
    for (let i = 0; i < positions.count; i++) {
      const x = positions.getX(i);
      const y = positions.getY(i);
      const waveZ = Math.sin(x * 3 + time * 2) * 0.015 +
                    Math.cos(y * 2.5 + time * 1.5) * 0.01 +
                    Math.sin((x + y) * 2 + time * 3) * 0.008;
      positions.setZ(i, waveZ);
    }
    positions.needsUpdate = true;
  }

  // ==================== 鼠标交互 ====================
  let mouseX = 0, mouseY = 0;
  let targetRotX = 0, targetRotY = 0;

  document.addEventListener('mousemove', (e) => {
    mouseX = (e.clientX / innerWidth - 0.5) * 2;
    mouseY = (e.clientY / innerHeight - 0.5) * 2;
  });

  document.addEventListener('touchmove', (e) => {
    e.preventDefault();
    mouseX = (e.touches[0].clientX / innerWidth - 0.5) * 2;
    mouseY = (e.touches[0].clientY / innerHeight - 0.5) * 2;
  }, { passive: false });

  // ==================== 动画循环 ====================
  const clock = new THREE.Clock();
  let frameCount = 0;

  function animate() {
    requestAnimationFrame(animate);
    const time = clock.getElapsedTime();
    frameCount++;

    // 平滑相机跟随鼠标
    targetRotY += (mouseX * 0.5 - targetRotY) * 0.03;
    targetRotX += (mouseY * 0.3 - targetRotX) * 0.03;

    camera.position.x = Math.sin(targetRotY) * 8;
    camera.position.z = Math.cos(targetRotY) * 8;
    camera.position.y = 3 + targetRotX * 2;
    camera.lookAt(0, TANK_H * 0.4 + TANK_Y, 0);

    // Boids 更新
    fishSchool.forEach(fish => {
      fish.flock(fishSchool);
      fish.avoidBounds();
      fish.avoidPlants();
      fish.update();
    });

    // 水草更新
    updateSeaweeds();

    // 气泡更新
    bubbles.forEach(b => {
      b.userData.phase += 0.02;
      b.position.y += b.userData.speed;
      b.position.x += Math.sin(b.userData.phase) * 0.003;
      b.position.z += Math.cos(b.userData.phase * 0.7) * 0.002;

      if (b.position.y > TANK_Y + TANK_H - 0.2) {
        b.position.y = TANK_Y + 0.1;
        b.position.x = (Math.random() - 0.5) * TANK_W * 0.6;
        b.position.z = (Math.random() - 0.5) * TANK_D * 0.6;
      }
      b.material.opacity = 0.3 + Math.sin(b.userData.phase) * 0.2;
    });

    // 水面
    updateWater(time);

    // 灯光微动
    waterLight.intensity = 1.5 + Math.sin(time * 2) * 0.3;

    renderer.render(scene, camera);
  }

  animate();

  // ==================== 响应式 ====================
  window.addEventListener('resize', () => {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });
})();
</script>
</body>
</html>
        
编辑器加载中
预览
控制台