纯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实战10)
ShaderJoy —— 纯 shader 实现立方体的渲染(含线框效果,虚线线框效果),带你了解渲染管线内部细节和原理GLSL