当前位置: 首页 > news >正文

three.js实现随机山脉波纹效果

three.js小白的学习之路。

好久没有更新了,今天分享一个简单的小效果,可以放在网站首页顶部当个背景展示,大概效果如下所示:

下面来分步介绍一下。

一、生成平面

首先,是基于一个平面实现全部的效果,使用PlaneGeometry生成一个平面,并且我们给它增加细分数,并使用线框模式:

相关代码:

import { ref, onMounted } from "vue";
import * as Three from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";const threeRef = ref();const scene = new Three.Scene();
const camera = new Three.PerspectiveCamera(75,window.innerWidth / window.innerHeight,0.1,10000
);
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);const geometry = new Three.PlaneGeometry(300, 300, 100, 100);const material = new Three.MeshBasicMaterial({color: new Three.Color("skyblue"),wireframe: true,
});
const mesh = new Three.Mesh(geometry, material);
mesh.position.set(0, 0, 0);
mesh.rotateX(Math.PI / 2);
scene.add(mesh);const renderer = new Three.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);new OrbitControls(camera, renderer.domElement);const render = () => {requestAnimationFrame(render);renderer.render(scene, camera);
};onMounted(() => {threeRef.value.appendChild(renderer.domElement);render();
});

很简单。但是由于所有的面都是三角形构成的,所以线框模式中间会有一道斜线,看着不是很舒服。于是将材质换成点材质:

相关代码:

const material = new Three.PointsMaterial({color: new Three.Color("skyblue"),
});
const mesh = new Three.Points(geometry, material);

看着舒服点,但是是个正方形的点,使用shader稍稍改进一下:

相关代码:

const material = new Three.PointsMaterial({color: new Three.Color("skyblue"),size: 2,
});material.onBeforeCompile = (shader) => {shader.fragmentShader = shader.fragmentShader.replace("#include <premultiplied_alpha_fragment>",`#include <premultiplied_alpha_fragment>if(distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.5) discard;`);
};

看着顺眼多了。

二、变化Z方向位置

要想让其产生波纹,二维显然已经不再满足,于是我们需要将其Z方向的坐标值进行变化。

不妨我们先给Z方向加一个正弦值,并且与X坐标相关联。原因是因为每个坐标都加一样的正弦值,那不是和不加是一样的:

相关代码:

const position = geometry.attributes.position;
for (let i = 0; i < position.count; i++) {const x = position.getX(i);const z = position.getZ(i);position.setZ(i, Math.sin(x * 0.1) * 5);
}

当然,也可以再加一点其他的东西:

相关代码:

const position = geometry.attributes.position;
for (let i = 0; i < position.count; i++) {const x = position.getX(i);const y = position.getY(i);const z = position.getZ(i);position.setZ(i, z + (Math.sin(x * 0.05) + Math.cos(y * 0.05)) * 10);
}

看着是不是太规律了一点,不像是随机生成的数据点。要想其变得不规律,很简单,加噪声嘛。推荐一个噪声的库,simplex-noise,我们安装,导入:

npm i simplex-noise -s
import { createNoise2D } from "simplex-noise";const noise = createNoise2D();

将噪声函数代替原本单纯的sin、cos函数:

相关代码:

for (let i = 0; i < position.count; i++) {const x = position.getX(i);const y = position.getY(i);const z = noise(x / 100, y / 100) * 25;position.setZ(i, z);
}

注意:Z轴的坐标值一定是与X轴和Y轴强相关的,这样随机出来的坐标会很平滑。

三、动起来,鲜花为你开

创建一个方法,让其在render函数里面跟随关键帧每次调用,更新相关的位置信息,像风吹过一样。但是有个问题,肯定是根据noise函数中的x和y的坐标去动起来,如果不规定范围,就会越来越大或者越来越小,因此我们需要使用sin或cos函数进行处理一下x和y的坐标值:

相关代码:

const updatePos = () => {for (let i = 0; i < position.count; i++) {const x = position.getX(i);const y = position.getY(i);const xCos = Math.cos(Date.now() * 0.001 + x * 0.05) * 5;const ySin = Math.sin(Date.now() * 0.001 + y * 0.05) * 5;const z = noise(x / 100, y / 100) * 25;position.setZ(i, z + xCos + ySin);}position.needsUpdate = true;
};const render = () => {requestAnimationFrame(render);updatePos();renderer.render(scene, camera);
};

我们使用Date.now()时间戳当做变量,这样每一次的关键帧动画,值都不一样,且是连续的。

或许有人觉得不规律的还没有规律的好看,这个无所谓,看个人:

相关代码:

const updatePos = () => {for (let i = 0; i < position.count; i++) {const x = position.getX(i);const y = position.getY(i);const z = position.getZ(i);const xCos = Math.cos(Date.now() * 0.001 + x * 0.05) * 0.1;const ySin = Math.sin(Date.now() * 0.001 + y * 0.05) * 0.1;position.setZ(i, z + xCos + ySin);}position.needsUpdate = true;
};

这个需要调整一下幅值和频率,以及相关的逻辑,否则就会出现上面那种某一刻是平面的情况,请自行探索。

四、充当背景

我们将其放在已在一个页面中,充当首屏banner的背景:

当然,我这只是一个示意,大家可以通过实际的UI配合(高度、文字等等)与不同的摄像机角度等,达到自己想要的效果。

相关代码:

<template><div class="box"><div class="top"><div id="three" ref="threeRef" /><div class="text">动起来,鲜花为你开!</div></div><div class="container"></div></div>
</template><script setup lang="ts">
import { ref, onMounted } from "vue";
import * as Three from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { createNoise2D } from "simplex-noise";const threeRef = ref();
const noise = createNoise2D();const scene = new Three.Scene();
const camera = new Three.PerspectiveCamera(75,window.innerWidth / window.innerHeight,0.1,10000
);
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);const geometry = new Three.PlaneGeometry(300, 300, 100, 100);const position = geometry.attributes.position;const material = new Three.PointsMaterial({color: new Three.Color("skyblue"),size: 2,
});material.onBeforeCompile = (shader) => {shader.fragmentShader = shader.fragmentShader.replace("#include <premultiplied_alpha_fragment>",`#include <premultiplied_alpha_fragment>if(distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.5) discard;`);
};const mesh = new Three.Points(geometry, material);
mesh.position.set(0, 0, 0);
mesh.rotateX(Math.PI / 2);
scene.add(mesh);const renderer = new Three.WebGLRenderer();new OrbitControls(camera, renderer.domElement);const updatePos = () => {for (let i = 0; i < position.count; i++) {const x = position.getX(i);const y = position.getY(i);const xCos = Math.cos(Date.now() * 0.001 + x * 0.05) * 5;const ySin = Math.sin(Date.now() * 0.001 + y * 0.05) * 5;const z = noise(x / 100, y / 100) * 25;position.setZ(i, z + xCos + ySin);}position.needsUpdate = true;
};const render = () => {renderer.setSize(threeRef.value.clientWidth, threeRef.value.clientHeight);requestAnimationFrame(render);updatePos();renderer.render(scene, camera);
};onMounted(() => {threeRef.value.appendChild(renderer.domElement);render();
});
</script><style scoped>
.box {width: 100vw;height: 100vh;overflow: hidden;.top {width: 100%;height: 20%;position: relative;#three {width: 100%;height: 100%;overflow: hidden;}.text {text-align: center;width: 100%;line-height: 20vh;font-size: 48px;color: white;position: absolute;top: 0;left: 0;pointer-events: none;}}
}
</style>

有几个点需要注意

1.renderer的size大小(相机也应该是,这里我偷懒了),最好根据实际画布大小进行设定,相机有一个camera.updateProjectionMatrix方法用于更新相机参数;

2.绝对定位最好让文字的容器去做,具体大家可以试一下;

3.如果还想让3D画布的轨道控制器可以使用,需要给text容器加上pointer-ecents: none;开启鼠标穿透事件,不然鼠标到达不了3D画布。

http://www.lryc.cn/news/607554.html

相关文章:

  • 【LeetCode刷题指南】--单值二叉树,相同的树
  • RustFS:高性能文件存储与部署解决方案(MinIO替代方案)
  • session和cookie作用详解
  • Solana:解决Anchor Build编译程序报错 no method named `source_file` found for struct
  • 设计模式1:创建型模式
  • 后台管理系统权限管理:前端实现详解
  • PDFsam免费开源!PDF分割合并工具
  • unity学习——视觉小说开发(一)
  • AI应用UX设计:让技术更懂用户
  • Android Jetpack 系列(五)Room 本地数据库实战详解
  • 第一个大语言模型的微调
  • Transformer架构全解析:搭建AI的“神经网络大厦“
  • Spring之【循环引用】
  • 插件升级:Chat/Builder 合并,支持自定义 Agent、MCP、Rules
  • 小学阶段的学习机推荐:科大讯飞T30、Lumie 10学习机暑期16项AI功能升级
  • 代码随想录day52图论3
  • Effective C++ 条款15:在资源管理类中提供对原始资源的访问
  • ICML 2025 | 深度剖析时序 Transformer:为何有效,瓶颈何在?
  • qt中的手势
  • STM32学习记录--Day5
  • 操作系统-lecture4(进程的调度)
  • win10 VC++6.0 应用程序无法正常运行 0xc0000142,应用程序无法正常启动,报错“0xc0000142”,解决办法
  • 深度解读 CSGHub:开源协议、核心功能与产品定位
  • Springboot 配置 doris 连接
  • Spring Boot 异步执行方式全解析:@Async、CompletableFuture 与 TaskExecutor 对比
  • JavaWeb笔记2-JavaScriptVueAjax
  • 备案主体更换期间网站可以访问吗
  • opencv-python的GPU调用
  • 树莓派GPIO介绍 + LED控制
  • 智能Agent场景实战指南 Day 28:Agent成本控制与商业模式