今天一起看一个教程,包含动态水面、真实天空和环境光照的 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
加载水面法线纹理。 - 纹理设置为重复模式,确保贴图不会拉伸变形。
- 加载完成后调用
initScene
和animate
进入主流程。
初始化天空盒与太阳位置
引入 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
对象生成平面水面材质。 - 使用
add
将Water
添加到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)