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

发布于:2024-04-28 ⋅ 阅读:(24) ⋅ 点赞:(0)

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

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

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

实现功能

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

那么! 开始吧!

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)

        }

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

image.png

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);
        }
image.png

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

使用 s i n sin 函数,模拟海面起伏,当然,你也可以用 c o s cos

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

image.png

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

起伏太大了,修改一下公式为 f ( y ) = ( s i n ( x 1.0 / S C A L E + e l a p s e d T i m e 1.0 ) + s i n ( x 2.3 / S C A L E + e l a p s e d T i m e 1.5 ) + s i n ( x 3.3 / S C A L E + e l a p s e d T i m e 0.4 ) ) / 3.0 + ( s i n ( z 0.2 / S C A L E + u T i m e 1.8 ) + s i n ( z 1.8 / S C A L E + u T i m e 1.8 ) + s i n ( z 2.8 / S C A L E + u T i m e 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 ,控制海面起伏以及纹理位移,海平面最终代码

        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();
Document%20-%20Google%20Chrome%202024-04-28%2011-32-43_converted.gif

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;

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

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

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

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

image.png

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

// 首先我们写下 dx 和 dz的求导公式,该求导公式由上述 f ( y ) 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)

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

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

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

image.png

计算旋转轴,旋转轴可以根据向量的叉乘计算得出, V e c t o r 3 ( k x , 1 , k z ) V e c t o r 3 ( k x , 1 , k z ) 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)

最终效果

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

网站公告

今日签到

点亮在社区的每一天
去签到