很长一段时间没有在掘金发布新的文章了,开始觉得自己发的文章要么不能讲透彻,要么太简单没有必要。
今天用 three.js 模拟渲染海洋,我将点燃大海!!!我将带领读者们一步一步实现最终效果,源码。
超级详细!!!每一步超级详细!!!保证看完即会!!!
实现功能
波涛起伏的浪
海面的船只的高度应该适应海浪的高度
海面的船只应该会发生正确的几何变换(旋转,位移)
那么! 开始吧!
1. 初始化 three 项目。
1.1 初始化:相机:camera,渲染器:renderer,场景:scene
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
let renderer, camera, scene, controls, clock,lineHelper
// 初始化场景基础元素(渲染器,相机,场景,控制器等等)
{
// 渲染器初始化
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
// 相机初始化
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 10, 20);
// 窗口自适应
function resize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', resize, false);
// 场景
scene = new THREE.Scene();
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
clock = new THREE.Clock();
}
function render() {
requestAnimationFrame(render);
const elapsedTime = clock.getElapsedTime()
controls.update();
renderer.render(scene, camera);
}
1.2 添加基础三维对象,添加一个Box模拟海面的小船,添加一条方向为(0,1,0)的线,模拟小船方向
// 将三维对象加入场景
{
// 添加平行光
const light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(0, 10, 20)
scene.add(light);
// 添加平行光2
const light2 = new THREE.DirectionalLight(0xffffff, 0.1);
light2.position.set(-5, 5, -5)
scene.add(light2);
// 添加环境光
const light3 = new THREE.AmbientLight(0xffffff, 0.2)
scene.add(light3)
// 添加模拟小船
box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshLambertMaterial());
scene.add(box)
// 添加法线辅助器
const helperGeometry = new THREE.BufferGeometry()
helperGeometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array([0, 0, 0, 0, 5, 0]), 3))
const lineHelper = new THREE.LineSegments(helperGeometry, new THREE.MeshBasicMaterial({ color: 0xff0000, depthTest: false }))
scene.add(lineHelper)
}
到此为止,页面效果如下,
2. 创建海平面
第一步,创建基础平面,顶点数量设置为 100 * 100 便于后续修改顶点位置
// 创建海平面
let material
{
material = new THREE.ShaderMaterial({wireFrame:true});
const geometry = new THREE.PlaneGeometry(100, 100, 500, 500);
geometry.rotateX(-Math.PI / 2);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
第二步,给海平面优化一下。
使用 sinsinsin 函数,模拟海面起伏,当然,你也可以用 coscoscos
根据公式 f(y)=sin(x)+sin(z)f(y) = sin(x) + sin(z)f(y)=sin(x)+sin(z) 可以得到,任意坐标下,海平面的高度 yyy
给平面添加贴图,美化一下
起伏太大了,修改一下公式为 f(y)=(sin(x∗1.0/SCALE+elapsedTime∗1.0)+sin(x∗2.3/SCALE+elapsedTime∗1.5)+sin(x∗3.3/SCALE+elapsedTime∗0.4))/3.0+(sin(z∗0.2/SCALE+uTime∗1.8)+sin(z∗1.8/SCALE+uTime∗1.8)+sin(z∗2.8/SCALE+uTime∗0.8))/3.0;f(y) = (sin(x * 1.0 / SCALE + elapsedTime * 1.0) + sin(x * 2.3 / SCALE + elapsedTime * 1.5) + sin(x * 3.3 / SCALE + elapsedTime * 0.4)) / 3.0 +(sin(z * 0.2 / SCALE + uTime * 1.8) + sin(z * 1.8 / SCALE + uTime * 1.8) + sin(z * 2.8 / SCALE + uTime * 0.8)) / 3.0 ;f(y)=(sin(x∗1.0/SCALE+elapsedTime∗1.0)+sin(x∗2.3/SCALE+elapsedTime∗1.5)+sin(x∗3.3/SCALE+elapsedTime∗0.4))/3.0+(sin(z∗0.2/SCALE+uTime∗1.8)+sin(z∗1.8/SCALE+uTime∗1.8)+sin(z∗2.8/SCALE+uTime∗0.8))/3.0; ,非常好看
海面不会动?在着色器添加时间参数 uTime ,控制海面起伏以及纹理位移,海平面最终代码
const SCALE = 5 // 控制海面起伏程度
const vertexShader = `
#define SCALE ${SCALE}.0
#include <common>
#include <logdepthbuf_pars_vertex>
varying vec2 vUv;
uniform float uTime;
float calculateSurface(float x, float z) {
float y = 0.0;
// 多个三角函数的叠加,增加随机性
y += (sin(x * 1.0 / SCALE + uTime * 1.0) + sin(x * 2.3 / SCALE + uTime * 1.5) + sin(x * 3.3 / SCALE + uTime * 0.4)) / 3.0;
y += (sin(z * 0.2 / SCALE + uTime * 1.8) + sin(z * 1.8 / SCALE + uTime * 1.8) + sin(z * 2.8 / SCALE + uTime * 0.8)) / 3.0;
return y;
}
void main() {
vUv = uv;
vec3 pos = position;
pos.y += calculateSurface(pos.x, pos.z);
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
#include <logdepthbuf_vertex>
}
`;
const fragmentShader = `
#include <common>
#include <logdepthbuf_pars_fragment>
varying vec2 vUv;
uniform sampler2D uMap;
uniform float uTime;
uniform vec3 uColor;
void main() {
#include <logdepthbuf_fragment>
vec2 uv = vUv * 10.0 + vec2(uTime * -0.05);
// uv
uv.y += 0.01 * (sin(uv.x * 3.5 + uTime * 0.35) + sin(uv.x * 4.8 + uTime * 1.05) + sin(uv.x * 7.3 + uTime * 0.45)) / 3.0;
uv.x += 0.12 * (sin(uv.y * 4.0 + uTime * 0.5) + sin(uv.y * 6.8 + uTime * 0.75) + sin(uv.y * 11.3 + uTime * 0.2)) / 3.0;
uv.y += 0.12 * (sin(uv.x * 4.2 + uTime * 0.64) + sin(uv.x * 6.3 + uTime * 1.65) + sin(uv.x * 8.2 + uTime * 0.45)) / 3.0;
// 纹理采样
vec4 tex1 = texture2D(uMap, uv * 1.0);
vec4 tex2 = texture2D(uMap, uv * 1.0 + vec2(0.2));
vec3 blue = uColor;
gl_FragColor = vec4(blue + vec3(tex1.a * 0.9 - tex2.a * 0.02), 1.0);
}
`;
const texture = new THREE.TextureLoader().load('./textures/water.png');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
const uniforms = {
uMap: { value: texture },
uTime: { value: 0 },
uColor: { value: new THREE.Color('#0051da') },
depthTest: true,
depthWrite: true,
};
material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
wireframe: true
});
const geometry = new THREE.PlaneGeometry(100, 100, 500, 500);
geometry.rotateX(-Math.PI / 2);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
在动画帧函数中更新时间参数
material.uniforms.uTime.value = clock.getElapsedTime();
3. 根据海平面的起伏,更新小船的高度
想必大家已经知道如何实现这个功能了,根据海平面的生成函数,计算小船的高度。
const position = box.position
const { x, z } = position
const { sin, cos, atan } = Math
position.y = (sin(x * 1.0 / SCALE + elapsedTime * 1.0) + sin(x * 2.3 / SCALE + elapsedTime * 1.5) + sin(x * 3.3 / SCALE + elapsedTime * 0.4)) / 3.0;
position.y += (sin(z * 0.2 / SCALE + elapsedTime * 1.8) + sin(z * 1.8 / SCALE + elapsedTime * 1.8) + sin(z * 2.8 / SCALE + elapsedTime * 0.8)) / 3.0;
效果,小船已经可以随着海面起伏更新高度
4. 根据海面起伏的角度,更新小船的旋转信息,以及添加小船的加速度。
应该可以理解吧。不会画图,在二维斜面上,小船会倾斜,并且获得斜面的切线的速度,叠加到小船本身的速度上。
在三维中,需要计算在某个坐标处的,海平面的切面,以及切面的面法线。
// 首先我们写下 dx 和 dz的求导公式,该求导公式由上述 f(y) 的两个变量 x 和 z 分别进行求导得到。
function dx(x, t) {
const cos = Math.cos
return 1 / 3 * (cos(x / SCALE + t) / SCALE + cos(2.3 * x / SCALE + 1.5 * t) * 2.3 / SCALE + cos(3.3 * x / SCALE + 0.4 * t) * 3.3 / SCALE)
}
function dz(z, t) {
const cos = Math.cos
return 1 / 3 * (cos(0.2 * z / SCALE + 1.8 * t) * 0.2 / SCALE + cos(1.8 * z / SCALE + 1.8 * t) * 1.8 / SCALE + cos(2.8 * z / SCALE + 0.8 * t) * 2.8 / SCALE)
}
// 根据对应导数函数,求出x分量的斜率和z分量的斜率
// 求出 斜率kx 和斜率kz
const kx = dx(x, elapsedTime)
const kz = dz(z, elapsedTime)
// 根据斜率写出切面的面法线,如下图,橙色代表法线。蓝色代表斜率
const n = new THREE.Vector3(-kx, 1, -kz).normalize();
// 计算旋转轴,以及旋转角度
计算旋转轴,旋转轴可以根据向量的叉乘计算得出, Vector3(−kx,1,−kz)∗Vector3(kx,1,kz)
const axes = new THREE.Vector3().crossVectors(n, new THREE.Vector3(kx, 1, kz)).normalize()
计算旋转角度
function getAngleBetweenVectors(v1, v2, dotThreshold = 0.00005) {
let angle = 0;
const dot = v1.dot(v2);
if (dot > 1 - dotThreshold) {
angle = 0;
} else if (dot < dotThreshold - 1) {
angle = Math.PI;
} else {
angle = Math.acos(dot);
}
return angle;
}
const angle = getAngleBetweenVectors(new THREE.Vector3(0, 1, 0), n)
执行旋转操作
box.rotation.x = 0
box.rotation.y = 0
box.rotation.z = 0
box.rotateOnAxis(axes, -angle)
计算小船的加速度
// 小船基础速度
const speed = new THREE.Vector3(0,0,0)
// 机选小船加速度的方向
const dir = new THREE.Vector3().crossVectors(n, axes).normalize().divideScalar(100)
// 小船速度叠加了由于海平面倾斜带来的速度最终的速度
const newSpeed = speed.add(dir)
计算小船最终的位置
const endPosition = box.position.clone().addScaledVector(newSpeed, 1)
let y = (sin(x * 1.0 / SCALE + elapsedTime * 1.0) + sin(x * 2.3 / SCALE + elapsedTime * 1.5) + sin(x * 3.3 / SCALE + elapsedTime * 0.4)) / 3.0;
y += (sin(z * 0.2 / SCALE + elapsedTime * 1.8) + sin(z * 1.8 / SCALE + elapsedTime * 1.8) + sin(z * 2.8 / SCALE + elapsedTime * 0.8)) / 3.0;
const truePosition = new THREE.Vector3(endPosition.x, y, endPosition.z)
box.position.copy(truePosition)
最终效果