shadertoy转three.js中shader代码 + 高德地图GLCustomLayer贴地呼吸图实现

three.js小白的学习之路。

shadertoy与three.js中shader代码的转换

最近开始重新审视shadertoy网站,这里面有太多大佬们创造的奇奇怪怪的glsl代码片段,实现了各种绚丽多彩的图案。但是,shadertoy中预定义了很多uniforms变量,其中最令我困惑的就是iResolution与three.js中uv的关系。

在shadertoy中经常可以看到如下的代码片段:

vec2 fragUv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

上述代码的意思是,通过分辨率(iResolution)和片元的顶点位置信息,计算出每一个点的uv坐标信息,其中fragCoord就是gl_FragCoord。

但是在three.js中,我们可以简单地在顶点着色器中使用这么一行代码(注意:这里的uv是顶点着色器里的uv,是three.js提供的变量,和底下shader代码段里定义的vec2 uv不是一回事!):

vec2 vUv = uv;

three.js中会自动进行差值,获取每一个位置的uv信息。

那这两者之间,如何转换?翻阅了大佬们的一些文章,结合这一篇关于shadertoy的介绍,找到了答案:

vec2 fragUv = vUv - 0.5;

没座!(杨坤音) 就是这么简单。那么就可以将iResolution的x和y分量计算出来,对应相等即可:

vUv - 0.5 = (gl-FragCoord - 0.5 * iResolution.xy) / iResolution.y

进而推算出:

(vUv.x - 0.5) * iResolution.y = gl-FragCoord.x - 0.5 * iResolution.x

(vUv.y - 0.5) * iResolution.y = gl-FragCoord.y - 0.5 * iResolution.y

这不二元一次方程嘛,解出来的解为:

 iResolution.x = 2 * (gl-FragCoord.x - (vUv.x + 0.5) * gl-FragCoord.y / vUv.y)

iResolution.y = gl-FragCoord.y / vUv.y

其实推断出来 fragUv = vUv - 0.5 是一个比较麻烦的过程,可以参考我上面标注的那一篇博客。

shadertoy转换到three.js中的shader

这里参考了guowei大佬在shadertoy中的一个动效,其源码如下:


float circle(float radius, vec2 center, vec2 uv) {
	float d = distance(center, uv);
    return 1.0 - smoothstep(radius-1./iResolution.y, radius+1./iResolution.y, d);
}

vec2 angleRadius(vec2 uv) {
	float anglePixel = atan(uv.y, uv.x);
    float lengthPixel = length(uv);
    
    return vec2(anglePixel, lengthPixel);
}

float filterPositive(float n) {
    return smoothstep(0.0, 0.005, n);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = (fragCoord - 0.5 * iResolution.xy)/iResolution.y; //many thanks to FabriceNeyret2
    
    float radius = 0.3;
    float ringThick = 0.05;
    
    vec2 stPolar = angleRadius(uv);
    
    float sPolar = stPolar.x * 3.0 + iTime * 10.0;

    float cosSPolarTemp = cos(sPolar);
    float cosSPolar = filterPositive(cosSPolarTemp);
    
    vec3 color = vec3(cosSPolar);
    

    float inCircleAA = smoothstep(radius, radius + 0.005, angleRadius(uv).y); //AA version
    float smallCircleAA = smoothstep(radius - ringThick, radius - ringThick + 0.005, angleRadius(uv).y); //AA version
    vec3 col = 1.0 - vec3(inCircleAA);
    vec3 col_2 = 1.0 - vec3(smallCircleAA);
    vec3 colorGap = col - col_2;
    vec3 finalColor = color * colorGap;
    vec3 colorMask = vec3(10, 1.5, 1.0);
    finalColor /= 10.0;
    finalColor *= colorMask;
    
    float centerCircleAA = smoothstep(0.1, 0.1 + 0.005, angleRadius(uv).y); //AA version
    vec3 centerCircleColor = 1.0 - vec3(centerCircleAA);
    centerCircleColor /= 10.0;
    centerCircleColor *= colorMask;
    
    vec2 centerC = vec2(0.0, 0.0);
    float bubbleRadius = abs(sin(iTime * 3.0)) / 3.0;
    float bubbleCircleColor = circle(bubbleRadius, centerC, uv);
    vec4 bubbleColor = vec4(vec3(bubbleCircleColor) / 10.0 * colorMask, 1.0);
    
    fragColor = vec4(finalColor + centerCircleColor, 1.0);
    fragColor += bubbleColor;    
}

大佬的代码很神秘,我竟然看不懂!(哭死,有看懂的大佬帮忙解释一下)

结合上文的推导,将有关iResolution相关代码片段的进行替换,写到three.js中就应该是这样:

const time = { value: 0 };
const shader = {
  uniforms: {
    iTime: time,
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
  uniform float iTime;
  varying vec2 vUv;

  // 动态变化大小的圆
  float circle(float radius, vec2 center, vec2 uv) {
    float d = distance(center, uv);
    return 1.0 - smoothstep(radius-1. / (gl_FragCoord.y / vUv.y), radius+1. / (gl_FragCoord.y / vUv.y), d);
  }

  vec2 angleRadius(vec2 uv) {
    float anglePixel = atan(uv.y, uv.x);
    float lengthPixel = length(uv);
      
    return vec2(anglePixel, lengthPixel);
  }

  float filterPositive(float n) {
      return smoothstep(0.0, 0.005, n);
  }

  void main() {
    vec2 uv = vUv - 0.5;
   
    float radius = 0.3;
    float ringThick = 0.05;    
    vec2 stPolar = angleRadius(uv);
    
    float sPolar = stPolar.x * 3.0 + iTime * 10.0; // stPolar.x * n,n就是圆环的个数
  
    float cosSPolarTemp = cos(sPolar);
    float cosSPolar = filterPositive(cosSPolarTemp);
    
    vec3 color = vec3(cosSPolar);

    float inCircleAA = smoothstep(radius, radius + 0.005, angleRadius(uv).y);
    float smallCircleAA = smoothstep(radius - ringThick, radius - ringThick + 0.005, angleRadius(uv).y);
    vec3 col = 1.0 - vec3(inCircleAA);
    vec3 col_2 = 1.0 - vec3(smallCircleAA);
    vec3 colorGap = col - col_2;
    vec3 finalColor = color * colorGap;
    vec3 colorMask = vec3(10, 1.5, 1.0);
    finalColor /= 10.0;
    finalColor *= colorMask;
      
    float centerCircleAA = smoothstep(0.1, 0.1 + 0.005, angleRadius(uv).y);
    vec3 centerCircleColor = 1.0 - vec3(centerCircleAA);
    centerCircleColor /= 10.0;
    centerCircleColor *= colorMask;
    
    vec2 centerC = vec2(0.0, 0.0);
    float bubbleRadius = abs(sin(iTime * 3.0)) / 3.0;
    float bubbleCircleColor = circle(bubbleRadius, centerC, uv);

    vec4 bubbleColor = vec4(vec3(bubbleCircleColor) / 10.0 * colorMask, 1.0);
    
    gl_FragColor = vec4(finalColor + centerCircleColor, 1.0);
    gl_FragColor += bubbleColor;
  }
  `,
};

先搞一个平面试一试效果:

嗯,去掉所有的黑色,在片元着色器里面加上如下的代码:

// 在main函数的最后
if(gl_FragColor.x < 0.01) gl_FragColor.a = 0.0; // 黑色部分变成透明色

最终效果如下:

其他部分代码如下:

const geo = new Three.PlaneGeometry(500, 500, 100, 100);
const mat = new Three.ShaderMaterial({
  ...shader,
  transparent: true, // 一定要加,不然不透明
});
const mesh = new Three.Mesh(geo, mat);
scene.add(mesh);

const loop = () => {
  time.value += 0.03; // 动起来
  renderer.render(scene, camera);
  requestAnimationFrame(loop);
};

高德地图 + three.js 实现贴地呼吸动效

既然完成了转换,最近也在学高德地图的相关API,正好实现一个类似于我之前的波纹图效果的贴地呼吸图。

实现效果如下:

具体实现代码如下,其中基本的代码和我的这一篇博客一样,只不过根据这个模型增加了一个生成方法addModel:

import { onMounted } from "vue";
import * as Three from "three";
import AMapLoader from "@amap/amap-jsapi-loader";
import "@amap/amap-jsapi-types";

let map: AMap.Map;
let camera: Three.PerspectiveCamera,
  scene: Three.Scene,
  renderer: Three.WebGLRenderer;

const time = { value: 0 };

const animate = () => {
  time.value += 0.03;
  (map as any).render();
  requestAnimationFrame(animate);
};

const shader = {
  uniforms: {
    iTime: time,
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
  uniform float iTime;
  varying vec2 vUv;

  // 动态变化大小的圆
  float circle(float radius, vec2 center, vec2 uv) {
    float d = distance(center, uv);
    return 1.0 - smoothstep(radius-1. / (gl_FragCoord.y / vUv.y), radius+1. / (gl_FragCoord.y / vUv.y), d);
  }

  vec2 angleRadius(vec2 uv) {
    float anglePixel = atan(uv.y, uv.x);
    float lengthPixel = length(uv);
      
    return vec2(anglePixel, lengthPixel);
  }

  float filterPositive(float n) {
      return smoothstep(0.0, 0.005, n);
  }

  void main() {
    vec2 uv = vUv - 0.5;
   
    float radius = 0.3;
    float ringThick = 0.05;
    
    vec2 stPolar = angleRadius(uv);
    
    float sPolar = stPolar.x * 3.0 + iTime * 10.0; // stPolar.x * n,n就是圆环的个数
  
    float cosSPolarTemp = cos(sPolar);
    float cosSPolar = filterPositive(cosSPolarTemp);
    
    vec3 color = vec3(cosSPolar);

    float inCircleAA = smoothstep(radius, radius + 0.005, angleRadius(uv).y);
    float smallCircleAA = smoothstep(radius - ringThick, radius - ringThick + 0.005, angleRadius(uv).y);
    vec3 col = 1.0 - vec3(inCircleAA);
    vec3 col_2 = 1.0 - vec3(smallCircleAA);
    vec3 colorGap = col - col_2;
    vec3 finalColor = color * colorGap;
    vec3 colorMask = vec3(10, 1.5, 1.0);
    finalColor /= 10.0;
    finalColor *= colorMask;
      
    float centerCircleAA = smoothstep(0.1, 0.1 + 0.005, angleRadius(uv).y);
    vec3 centerCircleColor = 1.0 - vec3(centerCircleAA);
    centerCircleColor /= 10.0;
    centerCircleColor *= colorMask;
    
    vec2 centerC = vec2(0.0, 0.0);
    float bubbleRadius = abs(sin(iTime * 3.0)) / 3.0;
    float bubbleCircleColor = circle(bubbleRadius, centerC, uv);

    vec4 bubbleColor = vec4(vec3(bubbleCircleColor) / 10.0 * colorMask, 1.0);
    
    gl_FragColor = vec4(finalColor + centerCircleColor, 1.0);
    gl_FragColor += bubbleColor;
    if(gl_FragColor.x < 0.01) gl_FragColor.a = 0.0; // 黑色部分变成透明色
  }
  `,
};

const addModel = () => {
  const data = (map as any).customCoords.lngLatsToCoords([
    [116.52, 39.79],
    [116.54, 39.79],
    [116.56, 39.79],
    [116.58, 39.79],
    [116.52, 39.75],
    [116.54, 39.77],
    [116.56, 39.81],
    [116.58, 39.83],
  ]);

  const geo = new Three.PlaneGeometry(200, 200);
  const mat = new Three.ShaderMaterial({
    ...shader,
    transparent: true,
  });

  data.forEach((item: [number, number]) => {
    const mesh = new Three.Mesh(geo, mat);
    mesh.position.set(...item, 5);
    scene.add(mesh);
  });
};

const init = (gl: any) => {
  camera = new Three.PerspectiveCamera(
    60,
    window.innerWidth / window.innerHeight,
    10,
    1 << 30
  );
  renderer = new Three.WebGLRenderer({
    context: gl,
  });
  renderer.autoClear = false;

  scene = new Three.Scene();

  const axes = new Three.AxesHelper(10000);
  scene.add(axes);

  const aLight = new Three.AmbientLight(0xffffff, 0.3);
  scene.add(aLight);

  addModel();
};

const render = () => {
  renderer.resetState();
  (map as any).customCoords.setCenter([116.52, 39.79]);
  const {
    near,
    far,
    fov,
    up,
    lookAt,
    position,
  }: {
    near: number;
    far: number;
    fov: number;
    up: [number, number, number];
    lookAt: [number, number, number];
    position: [number, number, number];
  } = (map as any).customCoords.getCameraParams();
  camera.near = near;
  camera.far = far;
  camera.fov = fov;
  camera.position.set(...position);
  camera.up.set(...up);
  camera.lookAt(...lookAt);
  camera.updateProjectionMatrix();

  renderer.render(scene, camera);

  renderer.resetState();
};

const onWindowResize = () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener("resize", onWindowResize);

onMounted(() => {
  window._AMapSecurityConfig = {
    securityJsCode: "你的安全密钥",
  };

  AMapLoader.load({
    key: "你的key", // 申请好的Web端开发者Key,首次调用 load 时必填
    version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
  })
    .then(() => {
      map = new AMap.Map("container", {
        center: [116.54, 39.79],
        zooms: [3, 20],
        zoom: 14,
        viewMode: "3D",
        pitch: 50,
      });

      // 创建GL图层
      const glLayer = new AMap.GLCustomLayer({
        zIndex: 10,
        init,
        render,
      });
      map.add(glLayer);
      animate();
    })
    .catch((e) => {
      console.log(e);
    });
});

PS:有木有好的方法生成实景的山地模型?请大佬们不吝赐教! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值