记录--Threejs-着色器实现一个水波纹
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
hree.js 是一个基于 WebGL 的 JavaScript 3D 库,用于创建和渲染 3D 图形场景。
一、 图像渲染过程
1、webGL
webGL: WebGL 是一种基于 JavaScript API 的图形库,它允许在浏览器中进行高性能的 3D 图形渲染。webGL的渲染依赖于底层GPU的渲染能力。
通过获取<canvas>
元素获取WebGL的上下文,从而获得WebGL API和GPU。
GPU 图形处理器:处理图形计算的硬件。GPU运行着一个着色器小程序。包含两种类型的着色器程序,顶点着色器和片元着色器。
2、着色器
着色器:
3、坐标系
(1)模型空间:物体在其自身坐标系下的位置、大小、方向。
(2)世界空间:所有模型都放置在同一坐标系下的空间。每个物体都有唯一的坐标系。如three.js的AxesHelper是基于世界空间创建的坐标系。
(3)视图空间:相机所在的坐标系。简单来说就是以相机为原点,物体在相机眼中的位置。
(4)投影空间:将3D图形投影到二维屏幕上的坐标系。将3D坐标转化为2D坐标。
各个坐标系之间的转换:通过矩阵变换来完成。例如,将物体从模型空间转换到世界空间,可以使用模型变换矩阵将局部坐标转换为全局坐标。将物体从世界空间转换到视图空间,可以使用相机变换矩阵将全局坐标变换为相机坐标。最后,将视图空间中的坐标投影到屏幕上,可以使用投影变换矩阵将相机坐标变换为裁剪坐标。通过这些矩阵变换,可以将坐标从一个空间转换到另一个空间,从而实现3D图形的渲染和显示。
4、GPU渲染过程
(1)渲染管线:就是将3D坐标转化为屏幕像素(屏幕都是二维的,也就是二维坐标)的过程。分为以下几个阶段。
应用阶段:由CPU控制,主要负责数据的准备和处理。CPU将数据发送的GPU,包括图形的顶点坐标、纹理坐标、颜色信息等 。
几何阶段:运行在GPU中。将顶点坐标变换到屏幕空间中。
光栅化阶段:阶段运行在GPU中。光栅化阶段主要将渲染图元转换为像素,并进行颜色插值、纹理采样等处理,最终输出渲染像素。
(2)GPU具体渲染过程。
齐次裁剪空间:简单来说就是相机视锥体的范围。如下图
二、着色器材质
three.js中有两个着色器材质ShaderMaterial
和原始着色器材质RawShaderMaterial
,它是用着色器语言GLSL编写的程序,可以让我们自定义物体的着色器程序,从而实现复杂的效果。 1、ShaderMaterial:材质接收两个着色器,顶点着色器和片元着色器。着色器代码需要我们自己编写,来实现复杂的效果。来看下如何使用。
用着色器材质实现下面这个效果:
搭建目录结构和基础看这里
1、首先搭建一个three.js场景
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>threejs_collision</title>
<link rel="stylesheet" href="./asstes/css/style.css">
</head>
<body>
<script src="./main/index.js" type="module"></script>
</body>
</html>
*{
padding: 0;
margin: 0;
}
body,html {
background: green;
}
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.set(0, 0, 10);
//将相机添加到
scene.add(camera); //创建环境光,环境光会均匀的照亮场景中的所有物体。
const light = new THREE.AmbientLight(0x404040);
//将环境光添加到场景
scene.add(light);
// 创建平行光
const directionalLight = new THREE.DirectionalLight();
//设置光源位置
directionalLight.position.set(0, 5, 0);
//添加到场景
scene.add(directionalLight);
//设置光源投射阴影
directionalLight.castShadow= true // 创建渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
//开启渲染器阴影计算
renderer.shadowMap.enabled = true
//将canvas添加到body中
document.body.appendChild(renderer.domElement); // 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 轨道控制器的阻尼感
controls.enableDamping = true;
//辅助坐标轴
const axesHelp = new THREE.AxesHelper();
scene.add(axesHelp); const clock = new THREE.Clock()
//渲染函数
function render() {
//阻尼
controls.update()
let time = clock.getDelta();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
// 初始化渲染函数
render();
// 监听浏览器窗口尺寸变化
window.addEventListener('resize',() => {
//重新设置相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
//更新相机投影矩阵
camera.updateProjectionMatrix();
//重新设置渲染器尺寸
renderer.setSize(window.innerWidth,window.innerHeight);
//设置设备像素比
renderer.setPixelRatio(window.devicePixelRatio)
})
2、创建着色器材质
从上面动图可以看出,是一个平面贴了一张图,然后给这个平面加了wave的效果。
所以,先创建一个平面。
const planeGeometry = new THREE.PlaneGeometry(1,1,64,64);
引入贴图
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(
require('../asstes/img/texture/xx.jpeg')
)
创建一个shader文件夹存放着色器代码,新建一个两个.glsl文件,来写顶点着色器和片元着色器代码。如下:
在js文件中引入这两个着色器:
import vertexShaderText from '../shader/basic/vertex.glsl'
import fragmentShaderText from '../shader/basic/fragment.glsl'
创建着色器材质
const material = new THREE.ShaderMaterial({
// 顶点着色器
vertexShader:vertexShaderText,
// 片元着色器
fragmentShader:fragmentShaderText,
// 设置两面可见,默认只能看见一面
side:THREE.DoubleSide,
})
3、编写着色器代码
顶点着色器 使用的是着色器语言(GLSL),不会也没事,知道怎么接收参数,在哪写逻辑就够了。首先要有一个入口点,也就是下面的void main函数,对数据的处理就写在这里面。
shader中有三种类型的变量: uniforms, attributes, 和 varyings
uniforms:从应用程序(CPU)传到着色器的变量(GPU),顶点着色器和片元着色器都能访问,比如我们可以在ShaderMaterial传递uniforms,在着色器程序中接收。用法:接收:uniform float uTime;
attributes:与每个顶点关联的变量。例如,顶点位置,法线和顶点颜色都是存储在attributes中的数据。attributes 只 可以在顶点着色器中访问。用法:attribute vec3 position
。
Varyings:在顶点着色器和片元着色器中传递数据。可以将顶点着色器处理过的数据通过varyings传给片元着色器。用法:varying vec2 vUv
;
// 高精度浮点数
precision highp float;
void main(){
// vec4 四维向量
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// projectionMatrix 投影矩阵;viewMatrix 视图矩阵;modelMatrix 模型矩阵;跟上面提到的坐标系对应。这些都是内置的uniform,使用ShaderMaterial会自动到GLSL shader代码中。使用RawShaderMaterial不会自动添加,需要手动接收。
//gl_Position是一个内置变量,它表示经过投影、视图和模型变换后的顶点位置。
gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
片元着色器:
// 中等精度浮点数
precision mediump float;
void main(){
// gl_FragColor内置对象,片元的颜色值 vec4是个思维变量这里代表了红色分量、绿色分量、蓝色分量和透明度分量。
gl_FragColor = vec4(1.0,1.0,0.0,1.0);
}
效果:注意这个平面的颜色是片元着色器里gl_FragColor对象决定的,现在是写死的(当然也可以写活)。
接下来给这个平面添加wave的效果:这个平面在X、y轴,通过改变Z轴的坐标来使平面有上下波动的效果,这个波动的效果像不像正弦余弦曲线,可以通过sin,cos实现这个效果。可以通过Uniforms变量将数据传给顶点着色器和片元着色器。
const clock = new THREE.Clock()
//渲染函数
function render() {
let time = clock.getElapsedTime()
material.uniforms.uTime.value = time;
} const material = new THREE.ShaderMaterial({
uniforms:{
uTime:{
value:0
},
// 贴图
uTexture:{
value: texture
}
}
})
顶点着色器程序:
precision mediump float;
uniform float uTime;
//varying:从顶点着色器传递到片元着色器的变量。 将uv传递到片元着色器。uv是二维坐标,是物体顶点在纹理上的映射位置(相当于将一个3维物体展开后的对应的二维位置)。传递给片元着色器可以读取该坐标处的颜色,赋值给gl_FragColor,实现贴图效果。
varying vec2 vUv;
void main(){
vUv = uv;
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z = sin((modelPosition.x + uTime) * 10.0) * 0.05;
modelPosition.z += sin((modelPosition.y + uTime) * 10.0) * 0.05;
gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
precision mediump float;
// sampler2D类型的纹理变量
uniform sampler2D uTexture;
// 接收顶点着色器传来的uv
varying vec2 vUv;
void main(){
// texture2D是用于读取纹理颜色值的函数
vec4 textureColor = texture2D(uTexture,vUv);
gl_FragColor = textureColor; }
这样就是实现了以上效果。
如果是RawShaderMaterial材质,内置的uniform需要手动去接收,以上代码改成:
顶点着色器程序:
precision mediump float;
// 定义顶点
attribute vec3 position;
//定义位置参数
attribute vec2 uv;
// 传入投影矩阵
uniform mat4 projectionMatrix;
// 传入视图矩阵
uniform mat4 viewMatrix;
// 传入模型矩阵
uniform mat4 modelMatrix;
//接收着色器材质传递的时间参数
uniform float uTime;
// uv传递到片元着色器 varying是从顶点着色器传递到片元着色器的变量
varying vec2 vUv;
void main(){
vUv = uv;
vec4 modelPosition = modelMatrix * vec4(position,1.0);
modelPosition.z = sin((modelPosition.x + uTime) * 10.0) * 0.05;
modelPosition.z += sin((modelPosition.y + uTime) * 10.0) * 0.05;
gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
三、着色器实现一个水波纹
水波纹相对于上面旗帜飘动的效果,多了些随机性。如水波的高度是变化的,波浪的起伏是随机的,高处和低处的颜色不一样,水波波动的大小、频率等。这里用到了一些随机函数。将这些随机性添加给波浪的高度来达到更真实的效果。下面定义了很多参数,这些参数可以自己去调节看看它们是什么作用。
const material = new THREE.ShaderMaterial({
vertexShader:vertexShaderText,
fragmentShader:fragmentShaderText,
side:THREE.DoubleSide,
uniforms:{
uTime:{
value:0
},
uWaresFrequency:{
value:params.uWaresFrequency
},
uScale:{
value:params.uScale
},
uNoiseFrequency:{
value:params.uNoiseFrequency
},
uNoiseScale:{
value: params.uNoiseScale
},
uXzScale:{
value: params.uXzScale
},
uLowColor:{
value:new THREE.Color(params.uLowColor)
},
uHighColor: {
value:new THREE.Color(params.uHighColor)
},
uOpacity:{
value:params.uOpacity
}
},
transparent: true
}) const plane = new THREE.Mesh(planeGeometry,material)
plane.rotation.x = -Math.PI / 2
scene.add(plane) // 将这些uniforms变量添加到gui在,方便看效果,找到最合适的值。
gui.add(params,'uWaresFrequency').min(1).max(50).step(0.1).onChange(val => {
material.uniforms.uWaresFrequency.value = val;
});
gui.add(params,'uScale').min(0).max(0.2).step(0.01).onChange(val => {
material.uniforms.uScale.value = val;
});
gui.add(params,'uNoiseFrequency').min(0).max(100).step(0.1).onChange(val => {
material.uniforms.uNoiseFrequency.value = val;
});
gui.add(params,'uNoiseScale').min(0).max(5).step(0.01).onChange(val => {
material.uniforms.uNoiseScale.value = val;
});
gui.add(params,'uXzScale').min(1).max(5).step(0.01).onChange(val => {
material.uniforms.uXzScale.value = val;
});
gui.addColor(params,'uLowColor').onFinishChange(val => {
material.uniforms.uLowColor.value = new THREE.Color(val)
})
gui.addColor(params,'uHighColor').onFinishChange(val => {
material.uniforms.uHighColor.value = new THREE.Color(val)
})
gui.add(params,'uOpacity').min(0).max(1).onChange(val => {
material.uniforms.uOpacity.value = val;
})
顶点着色器程序:里面的函数都是从这本书里抄的
uniform float uTime;
uniform float uWaresFrequency;
uniform float uScale;
uniform float uNoiseFrequency;
uniform float uNoiseScale;
uniform float uXzScale;
varying float vElevation; float random (vec2 st) {
return fract(sin(dot(st.xy,vec2(12.9898,78.233)))*43758.5453123);
}
// 旋转函数
vec2 rotate(vec2 uv, float rotation, vec2 mid)
{
return vec2(
cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x,
cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y
);
} // 2d噪声函数
float noise (in vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f*f*(3.0-2.0*f);
return mix(a, b, u.x) +
(c - a)* u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
// 随机函数
vec4 permute(vec4 x)
{
return mod(((x*34.0)+1.0)*x, 289.0);
}
vec2 fade(vec2 t)
{
return t*t*t*(t*(t*6.0-15.0)+10.0);
}
float cnoise(vec2 P)
{
vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
Pi = mod(Pi, 289.0); // To avoid truncation effects in permutation
vec4 ix = Pi.xzxz;
vec4 iy = Pi.yyww;
vec4 fx = Pf.xzxz;
vec4 fy = Pf.yyww;
vec4 i = permute(permute(ix) + iy);
vec4 gx = 2.0 * fract(i * 0.0243902439) - 1.0; // 1/41 = 0.024...
vec4 gy = abs(gx) - 0.5;
vec4 tx = floor(gx + 0.5);
gx = gx - tx;
vec2 g00 = vec2(gx.x,gy.x);
vec2 g10 = vec2(gx.y,gy.y);
vec2 g01 = vec2(gx.z,gy.z);
vec2 g11 = vec2(gx.w,gy.w);
vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
g00 *= norm.x;
g01 *= norm.y;
g10 *= norm.z;
g11 *= norm.w;
float n00 = dot(g00, vec2(fx.x, fy.x));
float n10 = dot(g10, vec2(fx.y, fy.y));
float n01 = dot(g01, vec2(fx.z, fy.z));
float n11 = dot(g11, vec2(fx.w, fy.w));
vec2 fade_xy = fade(Pf.xy);
vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
return 2.3 * n_xy;
} void main() {
vec4 modelPosition = modelMatrix * vec4(position,1.0);
// 波浪高度
float elevation = sin(modelPosition.x * uWaresFrequency) * sin(modelPosition.z * uWaresFrequency * uXzScale);
elevation += cnoise(vec2(modelPosition.xz*uNoiseFrequency+uTime))
*uNoiseScale;
elevation *= uScale;
// 传到片元着色器
vElevation = elevation;
modelPosition.y += elevation;
gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
varying float vElevation;
uniform vec3 uLowColor;
uniform vec3 uHighColor;
uniform float uOpacity;
void main(){
float a = (vElevation + 1.0) / 2.0;
// 混合颜色
vec3 color = mix(uLowColor,uHighColor,a);
gl_FragColor = vec4(color,uOpacity);
}
本文转载于:
https://juejin.cn/post/7248982532728864825
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。
记录--Threejs-着色器实现一个水波纹的更多相关文章
- Threejs着色器基本使用样例改造
<!DOCTYPE html> <html lang="en"> <head> <title>three.js webgl - bu ...
- DirectX11--深入理解Effects11、使用着色器反射机制(Shader Reflection)实现一个复杂Effects框架
前言 如果之前你是跟随本教程系列学习的话,应该能够初步了解Effects11(现FX11)的实现机制,并且可以编写一个简易的特效管理框架,但是随着特效种类的增多,要管理的着色器.资源等也随之变多.如果 ...
- Opengl_入门学习分享和记录_02_渲染管线(一)顶点着色器&片段着色器
写在前面的废话:今天俺又来了哈哈,真的好棒棒! 今天的内容:之前我们大概描述了,我们自己定义的顶点坐标是如何被加载到GPU之中,并且介绍了顶点缓冲对象VBO用于管理这一块内存.今天开始详细分析它的具体 ...
- WebGL——水波纹特效
大家好,今天我ccentry要做一个水波纹特效,我们来看看水波纹特效的做法.首先我们来看一下水波纹特效的效果是怎么样的,请看下图. 我们要做的就是类似这种纹理特效,那么我们来看看是如何制作的吧.首先鲫 ...
- stage3D基础二-----顶点和片段着色器(转)
来源:http://www.adobe.com/cn/devnet/flashplayer/articles/vertex-fragment-shaders.html 本教程将介绍着色器.着色器是 S ...
- 三角函数之美-水波纹载入LoadingView
一.前言 学习是要总结的.近期几天学习了画图相关的,可是使用的机会较少,如今又快要遗忘了,这次看了水波纹的绘制.认为十分有意思,还是 把实现的方法记录下来.技术无他,为手熟尔.还是要多练习,空淡误国, ...
- 片元着色器(Fragment Shader)被称为像素着色器(Pixel Shader),但
片元着色器(Fragment Shader)被称为像素着色器(Pixel Shader),但片元着色器是一个更合适的名字, 因为此时的片元并不是一个真正意义上的像素.
- Unity3d 着色器语法(Shader)
Shader "name" { [Properties] Subshaders [Fallback] } 定义了一个着色器.着色器拥有一个 Properties 的列表.着色器包含 ...
- OpenGL ES学习笔记(一)——基本用法、绘制流程与着色器编译
首先声明下,本文为笔者学习<OpenGL ES应用开发实践指南(Android卷)>的笔记,涉及的代码均出自原书,如有需要,请到原书指定源码地址下载. 在Android.iOS等移动平台上 ...
- Unity 内置着色器(转)
Unity包括超过40种内置的shader. 标准着色器家族 Normal Shader Family 这些着色器都是Unity基本的着色器.适用于大多数的不透明物体,如果想要物体有透明.发光效果等, ...
随机推荐
- clickhouse导入和导出
一.连接clickhouse--客户端连接default库clickhouse-client -h localhost --port 9001 -u default --password 123456 ...
- Spring Boot 单元测试笔记
1. 导入JUnit5测试框架 <dependency> <groupId>org.springframework.boot</groupId> <artif ...
- 了解一下基本的tcp代理配置
我们首先用一个简单例子了解一下基本的tcp代理配置 worker_processes 1; #nginx worker 数量 error_log logs/error.log; #指定错误日志文件路径 ...
- 你应该知道的提升Visual Studio开发能力的5个技巧
如果你像我一样,或许你也沉迷于开发者工具.这就是我喜欢 Visual Studio 的原因之一--它有无数的生产力技巧. 这篇文章将展示五个这样的技巧,这些技巧对我每天的工作都有帮助.请注意,这些仅适 ...
- JS Leetcode 264. 丑数 II 题解分析,当暴力无法暴力,让我们弃武从文了解三指针
壹 ❀ 引 我在JS Leetcode 263. 丑数 题解分析,来认识有趣的丑数吧一文中记录了简单难度的丑数题,那么这篇题解是它的升级版,题目来自LeetCode264. 丑数 II,题目描述如下: ...
- NC23803 DongDong认亲戚
题目链接 题目 题目描述 DongDong每年过春节都要回到老家探亲,然而DongDong记性并不好,没法想起谁是谁的亲戚(定义:若A和B是亲戚,B和C是亲戚,那么A和C也是亲戚),她只好求助于会编程 ...
- STC8H8K64U 的 USB 功能测试(未成功)
对 STC8H8K64U 的 USB 功能测试, 因为存在很多问题并且未能解释/解决, 就不写到系列里了, 把记录放上来抛砖引玉吧. 代码 测试代码下载地址 http://www.stcmcudata ...
- 【Unity3D】Transform组件
1 前言 每个游戏对象有且仅有一个 Transform 组件,Transform 组件保存了游戏对象的位置信息,用户可以通过操作 Transform 组件实现对游戏对象的平移.旋转.缩放等变换.每 ...
- 【Unity3D】MonoBehaviour的生命周期
1 前言 Unity3D 中可以给每个游戏对象添加脚本,这些脚本必须继承 MonoBehaviour,用户可以根据需要重写 MonoBehaviour 的部分生命周期函数,这些生命周期函数由系统自 ...
- React闭包陷阱
React闭包陷阱 React Hooks是React 16.8引入的一个新特性,其出现让React的函数组件也能够拥有状态和生命周期方法,其优势在于可以让我们在不编写类组件的情况下,更细粒度地复用状 ...