Three.js案例
water1

今天一起看一个教程,包含动态水面、真实天空和环境光照的 Three.js 场景。

先看下效果

初始化渲染器与场景

新建 main.js 文件,并添加如下基础初始化逻辑。

// main.js
let container, renderer, scene, camera;
 
container = document.createElement('div');
document.body.appendChild(container);
 
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
 
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.5, 3000000);
camera.position.set(0, 15, 0);
  • renderer 负责渲染整个 3D 画面,启用了抗锯齿与阴影。
  • scene 为所有对象的容器。
  • camera 设置视角和透视参数。

加载水面法线纹理

添加法线贴图用于模拟水波效果。

const loader = new THREE.TextureLoader();
let waterNormals = null;
 
loader.load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/896175/waternormals.jpg', function (texture) {
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
  waterNormals = texture;
  initScene();
  animate();
});
  • 使用 THREE.TextureLoader 加载水面法线纹理。
  • 纹理设置为重复模式,确保贴图不会拉伸变形。
  • 加载完成后调用 initSceneanimate 进入主流程。

初始化天空盒与太阳位置

引入 Sky.js,并配置其大气散射参数及太阳位置。

let sky, sunSphere, uniforms;
 
function initSky() {
  sky = new THREE.Sky();
  scene.add(sky.mesh);
 
  sunSphere = new THREE.Mesh(
    new THREE.SphereBufferGeometry(20, 16, 8),
    new THREE.MeshBasicMaterial({ color: 0xffffff })
  );
  sunSphere.visible = false;
  scene.add(sunSphere);
 
  uniforms = sky.uniforms;
  uniforms.turbidity.value = 0.01;
  uniforms.rayleigh.value = 0.0066;
  uniforms.luminance.value = 0.8;
  uniforms.mieCoefficient.value = 10.1;
  uniforms.mieDirectionalG.value = 0.824;
 
  moveSun();
}
 
function moveSun() {
  const azimuth = 0.45843;
  const inclination = 0.3011;
  const distance = 4500;
  const theta = Math.PI * (inclination - 0.5);
  const phi = 2 * Math.PI * (azimuth - 0.5);
 
  sunSphere.position.x = distance * Math.cos(phi);
  sunSphere.position.y = distance * Math.sin(phi) * Math.sin(theta);
  sunSphere.position.z = distance * Math.sin(phi) * Math.cos(theta);
 
  uniforms.sunPosition.value.copy(sunSphere.position);
}
  • Sky 使用预设大气模型模拟天空。
  • sunSphere 控制太阳方向光源位置。
  • moveSun 根据天球坐标设置太阳位置,并同步更新天空效果。

添加动态水面

基于 Water.js 创建可反射天空的动态水面。

let water, mirrorMesh;
 
function initWater() {
  water = new THREE.Water(renderer, camera, scene, {
    textureWidth: 512,
    textureHeight: 512,
    waterNormals: waterNormals,
    alpha: 1.0,
    sunDirection: uniforms.sunPosition.value.normalize(),
    sunColor: 0xf5ebce,
    waterColor: 0x5b899b,
    distortionScale: 15.0
  });
 
  mirrorMesh = new THREE.Mesh(
    new THREE.PlaneGeometry(4400, 4400, 120, 120),
    water.material
  );
  mirrorMesh.add(water);
  mirrorMesh.rotation.x = -Math.PI * 0.5;
  scene.add(mirrorMesh);
}
  • 通过 Water 对象生成平面水面材质。
  • 使用 addWater 添加到 mirrorMesh,确保渲染正常。

添加环境光与太阳光源

let light;
 
function initLight() {
  const ambient = new THREE.AmbientLight(0xf5ebce, 0.25);
  scene.add(ambient);
 
  light = new THREE.DirectionalLight(0xf5ebce, 0.8);
  light.position.copy(sunSphere.position);
  light.castShadow = true;
  light.shadow.mapSize.set(2048, 2048);
  light.shadow.bias = 0.0000001;
 
  light.shadow.camera = new THREE.PerspectiveCamera(40, 0.7, 4000, 4800);
  scene.add(light);
}
  • AmbientLight 提供基础光照,不受方向影响。

  • DirectionalLight 模拟太阳光,可投射阴影。

添加随机柱体对象

添加多个带有法线贴图的柱体模拟景物。

let rusty = loader.load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/896175/tex02.jpg');
rusty.wrapS = rusty.wrapT = THREE.RepeatWrapping;
 
function random(seed) {
  const x = Math.sin(seed) * 10000;
  return x - Math.floor(x);
}
 
function addCylinders() {
  const mat = new THREE.MeshPhongMaterial({
    map: rusty,
    specular: 0xf5ebce,
    shininess: 23,
    specularMap: rusty,
    shading: THREE.FlatShading
  });
 
  for (let i = 0; i < 110; i++) {
    const h = (random(i) + 1) * 50;
    const geo = new THREE.CylinderGeometry(5, 5, h, 4);
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.set((random(i+214)-0.5)*1500, h/2 - 5, -random(i*35)*1500 - 100);
    mesh.rotation.x = random(i) * Math.PI / 20;
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    scene.add(mesh);
  }
}
  • 使用 random(seed) 保证每次刷新形状一致。

  • 所有柱体设置法线贴图并启用阴影投射。

实现动态水面顶点变化与渲染循环

const clock = new THREE.Clock();
let time = 0;
 
function animate() {
  requestAnimationFrame(animate);
 
  const delta = clock.getDelta();
  time += delta * 0.5;
 
  // 动态修改 Plane 顶点高度(可选增强)
  if (mirrorMesh.geometry.vertices) {
    const v = mirrorMesh.geometry.vertices;
    for (let i = 0; i < v.length; i++) {
      v[i].z = Math.sin(i + time * -1) * 3;
    }
    mirrorMesh.geometry.verticesNeedUpdate = true;
    camera.position.y = v[7320]?.z * 1.5 + 14;
  }
 
  moveSun();
  water.material.uniforms.time.value -= 1.0 / 60.0;
  water.sunDirection = uniforms.sunPosition.value.normalize();
  light.position.copy(sunSphere.position);
 
  water.render();
  renderer.render(scene, camera);
}
  • 使用 clock 控制时间推进。

  • 顶点偏移模拟轻微水面起伏。

  • 动态更新太阳光位置与水面反射参数。

监听窗口尺寸变化

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
  • 确保页面在窗口尺寸变化后仍能正确显示。

汇总场景初始化

添加主入口 initScene 方法整合各功能模块:

function initScene() {
  initSky();
  initWater();
  initLight();
  addCylinders();
}

代码

https://codepen.io/xorxor_hu/pen/mOWbVG (opens in a new tab)