【百练shader】threeJs实现简单海浪效果

424 阅读10分钟

image.png

海浪demo[00-00-00--00-00-03].gif

您们的点赞、评论是我不断向前的动力

1、创建平面

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.z = 5
camera.position.y = 5
const wave = new THREE.PlaneGeometry(6, 2, 100)
const material = new THREE.MeshStandardMaterial()
const waveMesh = new THREE.Mesh(wave, material)
waveMesh.rotation.x = -Math.PI / 2;
scene.add(waveMesh)

Image.png

2、创建材质

import vertexShader from './main.vert'
import fragmentShader from './main.frag'

const material = new THREE.ShaderMaterial({
  fragmentShader: fragmentShader,
  vertexShader: vertexShader,
  uniforms: {
    uTime: { value: 0 },
  },
  side: THREE.DoubleSide,
})

顶点着色器 添加波浪浮动

uniform float uTime;
varying vec2 vUv;
void main(){
	vec4 mPosition=modelMatrix*vec4(position,1.);
	// mPosition.y+=sin(mPosition.x*3.-uTime)/5.;
	// mPosition.y+=cos(mPosition.z*3.-uTime)/10.;
	mPosition.y+=sin(mPosition.x*3.)/4.;
	mPosition.y+=cos(mPosition.z*3.)/4.;
	gl_Position=projectionMatrix*viewMatrix*mPosition;
	vUv = uv;
}

片段着色器 添加海浪颜色

varying vec2 vUv;
uniform float uTime;
void main(){
  vec3 seaCol = vec3(.1608,.251,.7608);
  gl_FragColor=vec4(seaCol,1.);
}

Image.png

3、片段着色器中创建波浪

参考之前的文章

uv 范围是 0~1  即 x y 最小值为0 最大值为1  比值 1:1

我们创建的平面 x为6 y为2 xy的比值 3:1

为了设置显示为同样的比例 st*=vec2(6.,2.);

使用 distance距离方法计算 xy 到 0点的距离

距离大于1 的像素都显示为白色 是因为超过1的值都为显示白色

vec2 st=vUv;
  st*=vec2(6.,2.);
  float strength=1.;
  float d=distance(st,vec2(0.));
  vec3 seaCol = vec3(d);
  gl_FragColor=vec4(seaCol,1.);

Image.png

4、对超出值超出1的像素做处理

利用 fract 函数对 d 取小数

会有一个扩散的效果 扩散的起点在(0.0, 0.0)左下角

void main(){
  vec2 st=vUv;
  st*=vec2(6.,2.);
  float strength=1.;
  float d=distance(st,vec2(0.));
  strength=fract(d);
  vec3 seaCol = vec3(strength);
  gl_FragColor=vec4(seaCol,1.);
}

Image.png

5、移动扩散的中心位置

在改变里uv比例之前 通过st - 0.5 把uv 范围修改为 -0.5~0.5

把扩散的起点移动到中心

void main(){
  vec2 st=vUv;
  st.y-=.5;
  st.x-=.5;
  st*=vec2(6.,2.);
  float strength=1.;
  float d=distance(st,vec2(0.));
  strength=fract(d);
  vec3 seaCol = vec3(strength);
  gl_FragColor=vec4(seaCol,1.);

Image.png

这时再将 d 减去传入的时间参数就可以让图形动起来了

strength=fract(d-uTime);

只取到右边的波浪效果 将x轴 向左偏移 st.x-=.5; => st.x +=.3;

Image.png

6、改变对比度

通过pow 幂运算 和 乘以1.5 提高光暗对比度 并抽离成 wave 方法

float wave(vec2 st){
  st.y-=.5;
  st.x+=.3;
  st*=vec2(6.,2.);
  float strength=1.;
  float d=distance(st,vec2(0.));
  // strength=fract(d-uTime);
  strength=fract(d);
  strength=pow(strength+.05,3.);
  strength*=1.5;
  return strength;
}
void main(){
  float strength=wave(vUv);
  vec3 seaCol=vec3(strength);
  // vec3 seaCol = vec3(.1608,.251,.7608) + strength;
  gl_FragColor=vec4(seaCol,1.);
}

Image.png

7、网格噪声

简单介绍一下voronoi noise 网格噪声

把空间分割成网格,计算每个像素点到它所在网格中的那个特征点的距离。 将图像拆分3*3的小格子

定义特征点(0.5,0.5)

计算当前网格中的像素到特征点的距离

如何计算像素到特征点距离?

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main(){
    vec2 st=gl_FragCoord.xy/u_resolution.xy;
    // st.x *= u_resolution.x/u_resolution.y;
    vec3 color=vec3(.0);
   
    st*=3.;
    vec2 i_st=floor(st);
    vec2 f_st=fract(st);
    vec2 point=vec2(.0);
    // 网格中的特征点
    point=.5;
    // 网格中的像素到特征点的距离
    vec2 diff=point-f_st;
    float dist=length(diff);
   
    color+=dist;
    // 绘制中心点
    color+=1.-step(.02,dist);
    // 绘制网格
    color.r+=step(.98,f_st.x)+step(.98,f_st.y);
    gl_FragColor=vec4(color,1.);
}

Image.png

random2 会返回 0.0~1.0之间的小数

通过random与sin函数 让特征点在当前网格偏移而不超出网格范围

vec2 random2(vec2 p){
    return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}
void main(){
    vec2 st=gl_FragCoord.xy/u_resolution.xy;
    // st.x *= u_resolution.x/u_resolution.y;
    vec3 color=vec3(.0);
   
    st*=3.;
    vec2 i_st=floor(st);
    vec2 f_st=fract(st);
    vec2 point=random2(i_st); // 新增
    // 网格中的特征点
    point=.5+.5*sin(6.2831*point); // 修改
    // 网格中的像素到特征点的距离
    vec2 diff=point-f_st;
    float dist=length(diff);
   
    color+=dist;
    // 绘制中心点
    color+=1.-step(.02,dist);
    // 绘制网格
    color.r+=step(.98,f_st.x)+step(.98,f_st.y);
    gl_FragColor=vec4(color,1.);
}

Image.png 需要增加每个网格内像素与相邻网格的特征点之间的距离

通过变两个for循环遍历,计算当前像素到其他相邻八个网格的特征点的距离,取最小值

void main(){
    vec2 st=gl_FragCoord.xy/u_resolution.xy;
    // st.x *= u_resolution.x/u_resolution.y;
    vec3 color=vec3(.0);
   
    st*=3.;
    vec2 i_st=floor(st);
    vec2 f_st=fract(st);
    float m_dist=1.;
   
    for(int y=-1;y<=1;y++){
        for(int x=-1;x<=1;x++){
             // 相邻网格
            vec2 neighbor=vec2(float(x),float(y));
            vec2 point=random2(i_st+neighbor);
           
            // 网格中的特征点
            point=.5+.5*sin(6.2831*point);// 修改
            // 网格中的像素到特征点的距离
            vec2 diff = neighbor + point - f_st;
            float dist=length(diff);
            // Keep the closer distance
            m_dist=min(m_dist,dist);
        }
    }
    vec2 point=random2(i_st);// 新增
  
    color+=m_dist;
    // 绘制中心点
    color+=1.-step(.02,m_dist);
    // 绘制网格
    color.r+=step(.98,f_st.x)+step(.98,f_st.y);
    gl_FragColor=vec4(color,1.);
}

Image.png

8、第二种方式实现不规则网格效果

多个旋转变形后的基础距离图叠加

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
    );
}
float voronoi(vec2 st){
    vec2 i_st=floor(st);
    vec2 f_st=fract(st);
    vec2 point=vec2(.5);
    vec2 diff=point-f_st;
    float dist=length(diff)+.1;
    return dist;
}
uv*=4.;
float dist1=voronoi(uv*vec2(1.1,1.3)+vec2(.0,.0/3.));
float dist2=voronoi(rotate(uv*vec2(1.1,1.3),PI*.25,vec2(.5)));
float dist3=voronoi(rotate(uv*vec2(1.5,.9),PI*-.13,vec2(.5)));
Image.png Image.png Image.png

叠加取最小值

dist=min(dist1,dist2);
dist=min(dist,dist3);
Image.png 还可以通过幂运算增加对比度
dist = pow(dist,2.);
Image.png 完整代码
#ifdef GL_ES
precision mediump float;
#endif
#define PI 3.14159
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
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
    );
}
float voronoi(vec2 st){
    vec2 i_st=floor(st);
    vec2 f_st=fract(st);
    vec2 point=vec2(.5);
    vec2 diff=point-f_st;
    float dist=length(diff)+.1;
    return dist;
}
void main(){
    vec2 st=gl_FragCoord.xy/u_resolution.xy;
    st.x*=u_resolution.x/u_resolution.y;
    vec3 color=vec3(.0);
    vec2 uv=st;
    uv*=4.;
    // uv+=u_time/15.;
    float dist=1.;
    // 移动的
    // float dist1=voronoi(uv*vec2(1.1,1.3)+vec2(.0,u_time/3.));
    // float dist2=voronoi(rotate(uv*vec2(1.1,1.3),PI*.25,vec2(.5)));
    // float dist3=voronoi(rotate(uv*vec2(1.5,.9)+u_time/3.,PI*-.13,vec2(.5)));
    float dist1=voronoi(uv*vec2(1.1,1.3)+vec2(.0,.0/3.));
    float dist2=voronoi(rotate(uv*vec2(1.1,1.3),PI*.25,vec2(.5)));
    float dist3=voronoi(rotate(uv*vec2(1.5,.9),PI*-.13,vec2(.5)));
    dist=min(dist1,dist2);
    dist=min(dist,dist3);
    // dist = pow(dist,2.);
    // dist = dist1;
    // dist = dist2;
    // dist = dist3;
    color+=dist;
    color+=vec3(.1529,.251,.8235);
    gl_FragColor=vec4(color,.8);
}

image.png

9、另一种voronoi图

前面是求像素到特征点 下面是求像素到边缘最小值

#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
vec2 random2(vec2 p){
    return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}
vec3 voronoi(vec2 st){
    vec2 n = floor(st);
    vec2 f = fract(st);
    vec2 mg, mr;
    float md = 8.0;
    for (int j= -1; j <= 1; j++) {
        for (int i= -1; i <= 1; i++) {
            vec2 g = vec2(float(i),float(j));
            vec2 o = random2( n + g );
            // o = 0.5 + 0.5*sin( u_time + 6.2831*o );
            o = 0.5 + 0.5*sin( 6.2831*o );
            vec2 r = g + o - f;
            float d = dot(r,r);
            if( d<md ) {
                md = d;
                mr = r;
                mg = g;
            }
        }
    }
    md = 8.0;
    for (int j= -2; j <= 2; j++) {
        for (int i= -2; i <= 2; i++) {
            vec2 g = mg + vec2(float(i),float(j));
            vec2 o = random2( n + g );
            // o = 0.5 + 0.5*sin( u_time + 6.2831*o );
            o = 0.5 + 0.5*sin( 6.2831*o );
            vec2 r = g + o - f;
            md = min(md, dot( 0.5*(mr+r), normalize(r-mr) ));
        }
    }
    return vec3(md, mr);
   
}
void main(){
    vec2 st=gl_FragCoord.xy/u_resolution.xy;
    // st.x *= u_resolution.x/u_resolution.y;
    vec3 color=vec3(.0);
   
    st*=3.;
    vec2 i_st=floor(st);
    vec2 f_st=fract(st);
    vec3 c=voronoi(st);
    color=c.x*vec3(1.);
    // 绘制网格
    color.r+=step(.98,f_st.x)+step(.98,f_st.y);
    gl_FragColor=vec4(color,1.);
}
Image.png 可以通过修改颜色得到简单类似长颈鹿皮肤纹理
color=mix(vec3(0.8431, 0.5412, 0.0196),vec3(0.5804, 0.251, 0.0118),c.x);
color=mix(vec3(1.),color,smoothstep(.01,.1,c.x));
Image.png Image.png

10、海浪拖尾

使用到前面提到的网格噪声 抽离一个新的网格噪声方法

float gridNoise(vec2 st){
  st*=vec2(5.,5.);
  vec2 i_st=floor(st);
  vec2 f_st=fract(st);
  float m_dist=1.;
  for(int y=-1;y<=1;y++){
    for(int x=-1;x<=1;x++){
      vec2 neighbor=vec2(float(x),float(y));
      vec2 point=random2(i_st+neighbor);
      point=.5+.5*sin(6.2831*point);
      vec2 diff=neighbor+point-f_st;
      float dist=length(diff);
      m_dist=min(m_dist,dist);
    }
  }
  m_dist=smoothstep(.1,.6,m_dist*.8);
  return m_dist;
}

Image.png 修改一下频率

float gridNoise(vec2 st){
  st*=vec2(1.,5.); // 修改
  st*=20.; // 新增
  .......
  return m_dist;
}

Image.png

11、与前面的扩散波叠加

void main(){
  float seaWave=wave(vUv);
  float waveTail=gridNoise(vUv);
  waveTail*=seaWave;
  waveTail+=seaWave;
  vec3 seaCol=vec3(waveTail);
  // vec3 seaCol=vec3(.1608,.251,.7608)+waveTail;
  // vec3 seaCol = vec3(.1608,.251,.7608);
  gl_FragColor=vec4(seaCol,1.);
}

Image.png

12、海浪头部不规则

这里利用到 fbm 分形布朗运动(Fractal Brownian Motion)

// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com


#ifdef GL_ES
precision mediump float;
#endif


uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;


float random (in vec2 st) {
    return fract(sin(dot(st.xy,
                         vec2(12.9898,78.233)))*
        43758.5453123);
}


// Based on Morgan McGuire @morgan3d
// https://www.shadertoy.com/view/4dS3Wd
float noise (in vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);


    // Four corners in 2D of a tile
    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;
}


#define OCTAVES 6
float fbm (in vec2 st) {
    // Initial values
    float value = 0.0;
    float amplitude = .5;
    float frequency = 0.;
    //
    // Loop of octaves
    for (int i = 0; i < OCTAVES; i++) {
        value += amplitude * noise(st);
        st *= 2.;
        amplitude *= .5;
    }
    return value;
}


void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st.x *= u_resolution.x/u_resolution.y;
    // st.x += u_time/5.;
    vec3 color = vec3(0.0);
    color += fbm(st*6.0);
    gl_FragColor = vec4(color,1.0);
}


Image.png

生成海浪时使用fbm返回结果 对uv x轴做偏移

float wave(vec2 st){
  float fbmdata=fbm(st*6.); //新增
  st.y-=.5; 
  // st.x+=.2; //新增
  st.x+=.2+fbmdata/7.; //新增
  st*=vec2(6.,2.);
  float strength=1.;
  float d=distance(st,vec2(0.));
  strength=fract(d-uTime);
  strength=fract(d);
  strength=pow(strength,3.);
  strength=min(1.,strength);
  strength*=1.2;
  return strength;
}

Image.png 可以调整偏移量

st.x+=.2+fbmdata/10.;

Image.png

13、上色+取消注释时间参数+对样式做些调整  让效果动起来

varying vec2 vUv;
uniform float uTime;
#define PI 3.14159265359;
vec2 random2(vec2 p){
  return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}
float random(vec2 p){
  return fract(sin(dot(p,vec2(12.9898,78.233)))*43758.5453123);
}
float noise(vec2 st){
  vec2 i=floor(st);
  vec2 f=fract(st);
  float a=random(i);
  float b=random(i+vec2(1.,0.));
  float c=random(i+vec2(0.,1.));
  float d=random(i+vec2(1.,1.));
  vec2 u=f*f*(3.-2.*f);
  return mix(a,b,u.x)+
  (c-a)*u.y*(1.-u.x)+
  (d-b)*u.x*u.y;
}
#define OCTAVES 6
float fbm(vec2 st){
  st.y -= uTime/4.; 
  float value=0.;
  float amplitude=.5;
  float frequency=0.;
  for(int i=0;i<OCTAVES;i++){
    value+=amplitude*noise(st);
    st*=2.;
    amplitude*=.5;
  }
  return value;
}
float wave(vec2 st){
  float fbmdata=fbm(st*6.);
  st.y-=.5;
  st.x+=.2;
  st.x+=+fbmdata/10.;
  st*=vec2(6.,2.);
  float strength=1.;
  
  float d=distance(st,vec2(0.));
  strength=fract(d-uTime);
  // strength=fract(d);
  strength=pow(strength,3.);
  strength=min(1.,strength);
  strength*=1.2;
  return strength;
}
float gridNoise(vec2 st){
  st*=vec2(2.,5.); // 修改
  st*=5.; // 新增
  vec2 i_st=floor(st);
  vec2 f_st=fract(st);
  float m_dist=1.;
  
  for(int y=-1;y<=1;y++){
    for(int x=-1;x<=1;x++){
      vec2 neighbor=vec2(float(x),float(y));
      vec2 point=random2(i_st+neighbor);
      point=.5+.5*sin(6.2831*point);
      vec2 diff=neighbor+point-f_st;
      float dist=length(diff);
      m_dist=min(m_dist,dist);
    }
  }
  m_dist=smoothstep(.1,.7,m_dist*.8);
  return m_dist;
}
void main(){
  float seaWave=wave(vUv);
  float waveTail=gridNoise(vUv);

  waveTail*=seaWave;
  waveTail+=seaWave;
  waveTail=pow(waveTail,2.);
  // vec3 seaCol=vec3(waveTail);
  vec3 seaCol=vec3(.1608,.251,.7608)+waveTail;
  // vec3 seaCol = vec3(.1608,.251,.7608);
  gl_FragColor=vec4(seaCol,1.);
}
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import vertexShader from './main.vert'
import fragmentShader from './main.frag'
import vertexShader2 from './main02.vert'
import fragmentShader2 from './main02.frag'

onMounted(() => {})
let scene = new THREE.Scene()
// 天空盒
const urls = ['/skybox/px.jpg', '/skybox/nx.jpg', '/skybox/py.jpg', '/skybox/ny.jpg', '/skybox/pz.jpg', '/skybox/nz.jpg']

const cubeLoader = new THREE.CubeTextureLoader()
scene.background = cubeLoader.load(urls)
const renderer = new THREE.WebGLRenderer({
  antialias: true, // 抗锯齿
  // alpha: true
})
renderer.setScissorTest(true)
renderer.setClearColor(0x000000)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.z = 1
camera.position.y = 6
// 添加相机控制器
new OrbitControls(camera, renderer!.domElement)

// 设置环境光
const ambientLight = new THREE.AmbientLight(0xffffff) // 环境光
scene.add(ambientLight)

// 平行光
const directionalLight = new THREE.DirectionalLight(0x000000, 1)
// 设置光源的方向
directionalLight.position.set(50, 50, 50)
scene.add(directionalLight)

// 波浪
const wave = new THREE.PlaneGeometry(6, 2, 100, 100)
const material = new THREE.ShaderMaterial({
  fragmentShader: fragmentShader,
  vertexShader: vertexShader,
  uniforms: {
    uTime: { value: 0 },
    // uColor: new THREE.Uniform(new THREE.Color('#70c1ff')),
  },
  side: THREE.DoubleSide,
  //   transparent: true,
  //   depthWrite: false,
  //   depthTest: false,
})
const waveMesh = new THREE.Mesh(wave, material)
waveMesh.rotation.x = -Math.PI / 2
scene.add(waveMesh)

// 水面
const water = new THREE.PlaneGeometry(2, 2, 100, 100)
const material2 = new THREE.ShaderMaterial({
  fragmentShader: fragmentShader2,
  vertexShader: vertexShader2,
  uniforms: {
    uTime: { value: 0 },
    // uColor: new THREE.Uniform(new THREE.Color('#70c1ff')),
  },
  side: THREE.DoubleSide,
  transparent: true,
  //   depthWrite: false,
  //   depthTest: false,
})
const waterMesh = new THREE.Mesh(water, material2)
waterMesh.rotation.x = -Math.PI / 2
waterMesh.position.z = 3
scene.add(waterMesh)

const render = (): void => {
  renderer.setScissor(0, 0, window.innerWidth, window.innerHeight)
  renderer.render(scene, camera)
}
const clock = new THREE.Clock()
const animate = (): void => {
  requestAnimationFrame(animate)
  // mesh.rotation.x += 0.01
  // mesh.rotation.y += 0.01
  material.uniforms.uTime.value = clock.getElapsedTime()
  material2.uniforms.uTime.value = clock.getElapsedTime()
  render()
}
animate()

const destroyThreejs = () => {
  try {
    renderer.dispose()
    renderer.forceContextLoss()

    // scene.traverse((child) => {

    //   if (child.material) {
    //     child.material.dispose();
    //   }
    //   if (child.geometry) {
    //     child.geometry.dispose();
    //   }
    //   child = null;
    // })
    // scene = null;
  } catch (e) {
    console.error('Failed to destroy threejs', e)
  }
}
onBeforeUnmount(() => {
  destroyThreejs()
  document.body.removeChild(renderer.domElement)
})
</script>

<template>
  <div class="demo"></div>
</template>

<style scoped></style>

海浪demo[00-00-00--00-00-03].gif 代码地址

xzw199509/shader-demo (github.com)

xzw199509/xiao-three-template (github.com)

后续再继续优化修改把 文章写的不多 欢迎大家多提点建议