纯shader实现动态时钟效果(three.js实战14)

Posted 点燃火柴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了纯shader实现动态时钟效果(three.js实战14)相关的知识,希望对你有一定的参考价值。

1. demo效果

如上图为时钟外观图片,实际效果秒针、分针、时针可以走动

2. 实现要点

2.1 绘制刻度

绘制刻度的思路是首先画三个圆,一个是刻度的最边缘,一个是较短刻度的内边缘,还有一个是较长刻度的内边缘,然后将圆分为60等分,在每一个圆上取被60等分的坐标值
接下来是绘制,绘制时可以发现每间隔4个蓝色较短刻度有一个白色较长刻度,以5为单位取余,当可以被5整除时绘制白色较长刻度,取余绘制蓝色较短刻度

//画刻度
void drawMeasure(vec2 st, inout vec4 outColor) {
  st = rotate2d( -0.105) * st;
  st *= 0.6; //乘以大于1的数缩小,乘以小于1的数放大
  //拓展 += 一个vec2可以进行平移
  vec2 outerVertexs[60];
  vec2 innerVertexs[60];
  vec2 innermostVertexs[60];
  float outerRadius = 0.5;
  float innerRadius = 0.44;
  float innermostRadius = 0.40;
  
  float angle = 0.0;

  float one_point_angle = PI * 2.0 / 60.0;

  for(int i = 0; i < 60; i++){
    angle += one_point_angle;
    float x  = cos(angle);
    float y  = sin(angle);
    outerVertexs[i] = vec2(cos(angle)*outerRadius,sin(angle)*outerRadius);
    innerVertexs[i] = vec2(cos(angle)*innerRadius,sin(angle)*innerRadius);
    innermostVertexs[i] = vec2(cos(angle)*innermostRadius,sin(angle)*innermostRadius);
  } 

  float pct = 0.0;

  //绘制刻度
  for(int i = 0; i < 60;i++){

    if(mod(float(i),5.0)==0.0){
      vec3 lineColor = vec3(0.93,0.98,1.0);

      pct = sdSegment(st,innermostVertexs[i],outerVertexs[i],0.004);

      //线段图层
      vec4 layer = vec4(lineColor.rgb, pct);

      //混合图层
      outColor = mix(outColor, layer, layer.a);
    
    }else{
      vec3 lineColor = vec3(0.17,0.97,1.0);

      pct = sdSegment(st,innerVertexs[i],outerVertexs[i],0.002);

      //刻度图层
      vec4 layer = vec4(lineColor.rgb, pct);

      //混合图层
      outColor = mix(outColor, layer, layer.a);
    }

  }
}

绘制线段函数

//绘制线段
float sdSegment( vec2 p, vec2 a, vec2 b, float lineWidth )
{
  // pa表示a点指向p点的向量, ba表示a点指向b点的向量
  vec2 pa = p-a, ba = b-a;
  // h表示pa在ba上投影的长度占ba长度的比例,限定到[0,1]
  float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
  // ba*h 是以a为起点,方向与ba相同,长度等于pa在ba方向上投影的长度的向量
  // pa视为斜边,ba*h是直角边,那么pa - ba*h则是另一条直角边,也就是从p点做垂线垂直于ab,该垂线的长度就是所求最短距离
  return pow(1.0-(length( pa - ba*h ) - lineWidth) ,300.0);
}

2.2 绘制秒针

//二维旋转矩阵
mat2 rotate2d(float _angle){
  return mat2(cos(_angle),-sin(_angle),sin(_angle),cos(_angle));
}

上面为实现旋转的矩阵,我们只需要绘制一个静态的秒针,然后使用该矩阵设置对应的旋转速度,就可以实现秒针转动

void drawSecondHand(vec2 st,inout vec4 outColor){
  //u_time前加负号使之变为顺时针旋转
  float angle = -u_time* PI * 2.0 / 60.0;
  st = rotate2d( angle) * st;

  float lineWidth = 0.002;
  float radius = 0.56;

  float x  = cos(0.0)*radius;
  float y  = sin(0.0)*radius;

  float x1  = cos(PI)*0.1;
  float y1  = sin(PI)*0.1;

  float pct = sdSegment (st,vec2(x,y),vec2(x1,y1),lineWidth);

  vec3 lineColor = vec3(0.17,0.97,1.0);

  //线段图层
  vec4 layer0 = vec4(lineColor.rgb, pct);//绘制图层
  vec4 layer1 = max(outColor, layer0);//相交部分

  vec4 layer2 = vec4(layer1.rgb, pct);
  outColor = mix(outColor, layer2, layer2.a);
}

2.3 绘制分针

与绘制秒针的思路相同,不过这一次不需要穿过圆心,直接从原点开始绘制,绘制的宽一些,旋转的速度慢一些,也就是与秒针相比角速度再除以60

void drawMinuteHand(vec2 st,inout vec4 outColor){
  //u_time前加负号使之变为顺时针旋转
  float angle = -u_time* PI * 2.0 / (60.0*60.0);
  st = rotate2d( angle) * st;

  float lineWidth = 0.018;
  float radius = 0.46;

  float x  = cos(0.0)*radius;
  float y  = sin(0.0)*radius;

  float pct = sdSegment (st,vec2(x,y),vec2(0,0),lineWidth);
        
  vec3 lineColor = vec3(0.17,0.97,1.0);

  vec4 layer0 = vec4(lineColor.rgb, pct);//绘制图层
  vec4 layer1 = max(outColor, layer0);//相交部分

  vec4 layer2 = vec4(layer1.rgb, pct);
  outColor = mix(outColor, layer2, layer2.a);
}

2.4 绘制时针

绘制时针与分针基本相同,不同的是它的长度与旋转速度,与分针相比旋转速度再除以60

void drawHourHand(vec2 st,inout vec4 outColor){
  //u_time前加负号使之变为顺时针旋转
  float angle = -u_time* PI * 2.0 / (60.0*60.0*60.0);
  st = rotate2d( angle) * st;

  float lineWidth = 0.018;
  float radius = 0.36;

  float x  = cos(0.6)*radius;
  float y  = sin(0.6)*radius;

  float pct = sdSegment (st,vec2(x,y),vec2(0,0),lineWidth);

  vec3 lineColor = vec3(0.17,0.97,1.0);

  vec4 layer0 = vec4(lineColor.rgb, pct);//绘制图层
  vec4 layer1 = max(outColor, layer0);//相交部分

  vec4 layer2 = vec4(layer1.rgb, pct);
  outColor = mix(outColor, layer2, layer2.a);
}

3. demo代码

<body>
  <div id="container"></div>
  <script type="text/javascript" src="../three/build/three.js"></script>

  <script>
    var container;
    var camera, scene, planeMesh, renderer;
    var clock = new THREE.Clock(); // 创建THREE.Clock对象
    var uniforms = {
      u_resolution: {
        type: "v2",
        value: new THREE.Vector2()
      },
      radius: {
        type: "f",
        value: 0.5
      },
      u_time: {
        type: "f",
        value: 0.5
      }
    };
    var vertexShader = `
    attribute vec3 position;
    void main() {
      gl_Position = vec4( position, 1.0 );
    }
    `
    var fragmentShader = `
    #define PI 3.1415926535897932384626433832795
    #ifdef GL_ES
    precision mediump float;
    #endif
    uniform vec2 u_resolution;
    uniform float u_time;

    //二维旋转矩阵
    mat2 rotate2d(float _angle){
      return mat2(cos(_angle),-sin(_angle),sin(_angle),cos(_angle));
    }
    
    //绘制线段
    float sdSegment( vec2 p, vec2 a, vec2 b, float lineWidth )
    {
      // pa表示a点指向p点的向量, ba表示a点指向b点的向量
      vec2 pa = p-a, ba = b-a;
      // h表示pa在ba上投影的长度占ba长度的比例,限定到[0,1]
      float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
      // ba*h 是以a为起点,方向与ba相同,长度等于pa在ba方向上投影的长度的向量
      // pa视为斜边,ba*h是直角边,那么pa - ba*h则是另一条直角边,也就是从p点做垂线垂直于ab,该垂线的长度就是所求最短距离
      return pow(1.0-(length( pa - ba*h ) - lineWidth) ,300.0);
    }


    void drawMinuteHand(vec2 st,inout vec4 outColor){
      //u_time前加负号使之变为顺时针旋转
      float angle = -u_time* PI * 2.0 / (60.0*60.0);
      st = rotate2d( angle) * st;

      float lineWidth = 0.018;
      float radius = 0.46;

      float x  = cos(0.0)*radius;
      float y  = sin(0.0)*radius;

      float pct = sdSegment (st,vec2(x,y),vec2(0,0),lineWidth);
            
      vec3 lineColor = vec3(0.17,0.97,1.0);

      vec4 layer0 = vec4(lineColor.rgb, pct);//绘制图层
      vec4 layer1 = max(outColor, layer0);//相交部分

      vec4 layer2 = vec4(layer1.rgb, pct);
      outColor = mix(outColor, layer2, layer2.a);
    }

    void drawHourHand(vec2 st,inout vec4 outColor){
      //u_time前加负号使之变为顺时针旋转
      float angle = -u_time* PI * 2.0 / (60.0*60.0*60.0);
      st = rotate2d( angle) * st;

      float lineWidth = 0.018;
      float radius = 0.36;

      float x  = cos(0.6)*radius;
      float y  = sin(0.6)*radius;

      float pct = sdSegment (st,vec2(x,y),vec2(0,0),lineWidth);
 
      vec3 lineColor = vec3(0.17,0.97,1.0);

      vec4 layer0 = vec4(lineColor.rgb, pct);//绘制图层
      vec4 layer1 = max(outColor, layer0);//相交部分

      vec4 layer2 = vec4(layer1.rgb, pct);
      outColor = mix(outColor, layer2, layer2.a);
    }

    void drawSecondHand(vec2 st,inout vec4 outColor){
      //u_time前加负号使之变为顺时针旋转
      float angle = -u_time* PI * 2.0 / 60.0;
      st = rotate2d( angle) * st;

      float lineWidth = 0.002;
      float radius = 0.56;

      float x  = cos(0.0)*radius;
      float y  = sin(0.0)*radius;

      float x1  = cos(PI)*0.1;
      float y1  = sin(PI)*0.1;

      float pct = sdSegment (st,vec2(x,y),vec2(x1,y1),lineWidth);

      vec3 lineColor = vec3(0.17,0.97,1.0);

      //线段图层
      vec4 layer0 = vec4(lineColor.rgb, pct);//绘制图层
      vec4 layer1 = max(outColor, layer0);//相交部分

      vec4 layer2 = vec4(layer1.rgb, pct);
      outColor = mix(outColor, layer2, layer2.a);
    }

    //画刻度
    void drawMeasure(vec2 st, inout vec4 outColor) {
      st = rotate2d( -0.105) * st;
      st *= 0.6; //乘以大于1的数缩小,乘以小于1的数放大
      //拓展 += 一个vec2可以进行平移
      vec2 outerVertexs[60];
      vec2 innerVertexs[60];
      vec2 innermostVertexs[60];
      float outerRadius = 0.5;
      float innerRadius = 0.44;
      float innermostRadius = 0.40;
      
      float angle = 0.0;

      float one_point_angle = PI * 2.0 / 60.0;

      for(int i = 0; i < 60; i++){
        angle += one_point_angle;
        float x  = cos(angle);
        float y  = sin(angle);
        outerVertexs[i] = vec2(cos(angle)*outerRadius,sin(angle)*outerRadius);
        innerVertexs[i] = vec2(cos(angle)*innerRadius,sin(angle)*innerRadius);
        innermostVertexs[i] = vec2(cos(angle)*innermostRadius,sin(angle)*innermostRadius);
      } 

      float pct = 0.0;

      //绘制刻度
      for(int i = 0; i < 60;i++){

        if(mod(float(i),5.0)==0.0){
          vec3 lineColor = vec3(0.93,0.98,1.0);

          pct = sdSegment(st,innermostVertexs[i],outerVertexs[i],0.004);

          //线段图层
          vec4 layer = vec4(lineColor.rgb, pct);

          //混合图层
          outColor = mix(outColor, layer, layer.a);
        
        }else{
          vec3 lineColor = vec3(0.17,0.97,1.0);

          pct = sdSegment(st,innerVertexs[i],outerVertexs[i],0.002);

          //刻度图层
          vec4 layer = vec4(lineColor.rgb, pct);

          //混合图层
          outColor = mix(outColor, layer, layer.a);
        }
 
      }
    }

    void main( void ) {

      //转换为窗口坐标[0,1],坐标原点在屏幕左下角
      //vec2 st = gl_FragCoord.xy/u_resolution.y;
      //窗口坐标调整为[-1,1],坐标原点在屏幕中心
      vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;



      vec4 lastColor = vec4(0.0);

      //绘制刻度
      drawMeasure(st,lastColor);

      //绘制时针
      drawHourHand(st,lastColor);

      //绘制分针
      drawMinuteHand(st,lastColor);

      //绘制秒针
      drawSecondHand(st,lastColor);

      gl_FragColor = lastColor;
    }
    `
    init();
    animate();

    function init() {
      container = document.getElementById('container');

      camera = new THREE.Camera();

      scene = new THREE.Scene();


      var geometry = new THREE.PlaneBufferGeometry(2, 2);

      var material = new THREE.RawShaderMaterial({
        uniforms: uniforms,
        vertexShader: vertexShader,
        fragmentShader: fragmentShader
      });

      planeMesh = new THREE.Mesh(geometry, material);


      scene.add(planeMesh);


      renderer = new THREE.WebGLRenderer();
      renderer.setSize(1000, 800); //设置窗口大小800px*800px

      container.appendChild(renderer.domElement);
      uniforms.u_resolution.value.x = renderer.domElement.width;
      uniforms.u_resolution.value.y = renderer.domElement.height;

    }


    function animate() {
      requestAnimationFrame(animate);

      const elapsed = clock.getElapsedTime();

      planeMesh.material.uniforms.u_time.value = clock.getElapsedTime();

      renderer.render(scene, camera);
    }
  </script>
</body>

以上是关于纯shader实现动态时钟效果(three.js实战14)的主要内容,如果未能解决你的问题,请参考以下文章

纯shader实现雷达扫描效果(three.js实战13)

纯shader实现移动的箭头(three.js实战15)

使用shader着色器程序创建扩散光圈效果(three.js实战10)

模拟楼宇扫光效果(three.js实战12)

ShaderJoy —— 纯 shader 实现立方体的渲染(含线框效果,虚线线框效果),带你了解渲染管线内部细节和原理GLSL

使用Three.js实现炫酷的赛博朋克风格3D数字地球大屏 🌐