很长一段时间没有在掘金发布新的文章了,开始觉得自己发的文章要么不能讲透彻,要么太简单没有必要。
今天用 three.js 模拟渲染海洋,我将点燃大海!!!我将带领读者们一步一步实现最终效果,源码。
超级详细!!!每一步超级详细!!!保证看完即会!!!
实现功能
- 波涛起伏的浪
- 海面的船只的高度应该适应海浪的高度
- 海面的船只应该会发生正确的几何变换(旋转,位移)
那么! 开始吧!
1. 初始化 three 项目。
1.1 初始化:相机:camera,渲染器:renderer,场景:scene
javascript
复制代码
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)的线,模拟小船方向
csharp
复制代码
// 将三维对象加入场景 { // 添加平行光 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 便于后续修改顶点位置
ini
复制代码
// 创建海平面 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); }
第二步,给海平面优化一下。
使用 ���sin 函数,模拟海面起伏,当然,你也可以用 ���cos
根据公式 �(�)=���(�)+���(�)f(y)=sin(x)+sin(z) 可以得到,任意坐标下,海平面的高度 �y
给平面添加贴图,美化一下
起伏太大了,修改一下公式为 �(�)=(���(�∗1.0/�����+�����������∗1.0)+���(�∗2.3/�����+�����������∗1.5)+���(�∗3.3/�����+�����������∗0.4))/3.0+(���(�∗0.2/�����+�����∗1.8)+���(�∗1.8/�����+�����∗1.8)+���(�∗2.8/�����+�����∗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 ,控制海面起伏以及纹理位移,海平面最终代码
ini
复制代码
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);
在动画帧函数中更新时间参数
ini
复制代码
material.uniforms.uTime.value = clock.getElapsedTime();
3. 根据海平面的起伏,更新小船的高度
想必大家已经知道如何实现这个功能了,根据海平面的生成函数,计算小船的高度。
arduino
复制代码
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 分别进行求导得到。
scss
复制代码
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分量的斜率
scss
复制代码
// 求出 斜率kx 和斜率kz const kx = dx(x, elapsedTime) const kz = dz(z, elapsedTime)
// 根据斜率写出切面的面法线,如下图,橙色代表法线。蓝色代表斜率
ini
复制代码
const n = new THREE.Vector3(-kx, 1, -kz).normalize();
// 计算旋转轴,以及旋转角度
计算旋转轴,旋转轴可以根据向量的叉乘计算得出, ������3(−��,1,−��)∗������3(��,1,��)Vector3(−kx,1,−kz)∗Vector3(kx,1,kz)
arduino
复制代码
const axes = new THREE.Vector3().crossVectors(n, new THREE.Vector3(kx, 1, kz)).normalize()
计算旋转角度
ini
复制代码
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)
执行旋转操作
ini
复制代码
box.rotation.x = 0 box.rotation.y = 0 box.rotation.z = 0 box.rotateOnAxis(axes, -angle)
计算小船的加速度
csharp
复制代码
// 小船基础速度 const speed = new THREE.Vector3(0,0,0) // 机选小船加速度的方向 const dir = new THREE.Vector3().crossVectors(n, axes).normalize().divideScalar(100) // 小船速度叠加了由于海平面倾斜带来的速度最终的速度 const newSpeed = speed.add(dir)
计算小船最终的位置
scss
复制代码
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)