three.js 模拟真实海洋(超详细教程,炫酷海洋)

本文详细介绍如何利用three.js库在JavaScript中实现逼真的海洋表面和船只动画,包括初始化项目、创建海平面、模拟船只随波浪起伏、旋转和加速度计算,以及实时渲染。
摘要由CSDN通过智能技术生成

很长一段时间没有在掘金发布新的文章了,开始觉得自己发的文章要么不能讲透彻,要么太简单没有必要。

今天用 three.js 模拟渲染海洋,我将点燃大海!!!我将带领读者们一步一步实现最终效果,源码

超级详细!!!每一步超级详细!!!保证看完即会!!!

实现功能

  1. 波涛起伏的浪
  2. 海面的船只的高度应该适应海浪的高度
  3. 海面的船只应该会发生正确的几何变换(旋转,位移)

那么! 开始吧!

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) }

到此为止,页面效果如下,

image.png

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); }

image.png

第二步,给海平面优化一下。

使用 ���sin 函数,模拟海面起伏,当然,你也可以用 ���cos

根据公式 �(�)=���(�)+���(�)f(y)=sin(x)+sin(z) 可以得到,任意坐标下,海平面的高度 �y

image.png

给平面添加贴图,美化一下 

image.png

起伏太大了,修改一下公式为 �(�)=(���(�∗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; ,非常好看

image.png

海面不会动?在着色器添加时间参数 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();

Document%20-%20Google%20Chrome%202024-04-28%2011-32-43_converted.gif

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;

效果,小船已经可以随着海面起伏更新高度

Document%20-%20Google%20Chrome%202024-04-28%2011-49-53_converted.gif

4. 根据海面起伏的角度,更新小船的旋转信息,以及添加小船的加速度。

应该可以理解吧。不会画图,在二维斜面上,小船会倾斜,并且获得斜面的切线的速度,叠加到小船本身的速度上。

image.png

在三维中,需要计算在某个坐标处的,海平面的切面,以及切面的面法线。

// 首先我们写下 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)

// 根据斜率写出切面的面法线,如下图,橙色代表法线。蓝色代表斜率

image.png

 

ini

复制代码

const n = new THREE.Vector3(-kx, 1, -kz).normalize();

// 计算旋转轴,以及旋转角度

image.png

计算旋转轴,旋转轴可以根据向量的叉乘计算得出, ������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)

最终效果

Document%20-%20Google%20Chrome%202024-04-28%2014-20-17_converted.gif

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
three.js是一个基于JavaScript的WebGL库,可以用于创建和渲染复杂的3D图形和动画。要模拟海洋,我们可以使用three.js的一些功能和技术。 首先,我们需要创建一个可视化的海洋场景。我们可以使用一个平面来代表海洋的水面,并给它一个蓝色的材质,以模拟海水的颜色。我们还可以在场景中添加一些光源,如太阳光或点光源,来模拟阳光照射到海面上。 为了使海洋真实,我们可以添加一些波浪效果。我们可以使用three.js中的顶点着色器和片段着色器来创建一个水波的效果。通过在顶点着色器中对海洋的顶点进行位移,并在片段着色器中对颜色进行修改,我们可以模拟海面上波浪的移动和变化。 为了给海洋增加一些动态效果,我们可以在场景中添加一些物体,如船只或鱼群。我们可以使用three.js中的模型加载器来导入3D模型,并将其放置在海面上。我们还可以给这些物体添加一些动画效果,如旋转、移动或飞行,以模拟它们在海洋中的行为。 最后,为了增强海洋真实感,我们可以在海面上添加一些粒子效果,如水花、喷泉或浪花。我们可以使用three.js中的粒子系统来创建这些效果,以模拟海浪的喷溅和水花的飞溅。 综上所述,使用three.js可以通过创建海洋场景、添加波浪效果、引入动态物体和粒子效果等方法来模拟海洋的效果。这些功能和技术可以使海洋场景更加真实、生动,并提供更好的用户体验。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值