前言:本文主要分以下几个部分阐述功能的核心实现。
1、 四棱锥的生成
2、物体的平移(也就是弹跳,本质上来说就是控制物体的渲染位置)
3、物体的绕自身中心轴旋转
4、Cesium drawCommand下的纹理贴图。
5、光带扫描(也有人叫动态泛光,这种效果在啥子智慧城市出现的比较多)

静态效果图

在这里插入图片描述

四棱锥的生成

在这里插入图片描述

上图可知, 四个边缘上的点是同一Y值,拉高中心点的y值或者减少 中心点的y值就可以形成一个正的四棱锥或者倒着的四棱锥,没有太值得说明的地方,如果有,那这篇文章也许并不适合你。此部分可以去看看我之前的几个博客。不重复解读了。这里创建一个以center 【116.138641,23.814026】经纬度高度为0的点为中心,经纬度加或减2高度为300000的点作为四个顶点构建的倒四棱锥。 当然,这个显示参数是可以任意修改的。

	createAnPyramidGeometry() {
        //  处理 顶点数据
        let positions = []
        let indices = []
        let st = []
        let __GROUND_HEIGHT = 0
        let __BASE_HEIGHT = 300000
        let center = [116.138641,23.814026 ]
        let point1 = vector2Add(center, [-2.0, 2.0])
        let point2 = vector2Add(center, [2.0, 2.0])
        let point3 = vector2Add(center, [-2.0, -2.0])
        let point4 = vector2Add(center, [2.0, -2.0])

        // this.centerPos = new Float32Array(...transformPos(center, __GROUND_HEIGHT))

        positions.push(...transformPos(center, __GROUND_HEIGHT), ...transformPos(point1, __BASE_HEIGHT), ...transformPos(point2, __BASE_HEIGHT), ...transformPos(point3, __BASE_HEIGHT), ...transformPos(point4, __BASE_HEIGHT))
        
        this.leftTopPos = new Cesium.Cartesian3(...transformPos(point1,__BASE_HEIGHT));

        indices = [ 
            0,1,2,
            0,1,3,
            0,3,4,
            0,2,4,
            1,2,3,
            2,3,4
        ]
        
        function vector2Add(vec1, vec2) {
            return [vec1[0] + vec2[0], vec1[1] + vec2[1]]
        }

        function transformPos(lonlat, height) {
            let pos = Cesium.Cartesian3.fromDegrees(lonlat[0], lonlat[1], height)
            return [pos.x, pos.y, pos.z]
        }

        //  必要明确的告诉 框架 去怎么样 处理 你提供进去的数据.
        let geometry = new Cesium.Geometry({
            //  告诉cesium 你需要怎样读 数组里面的内容
            attributes: {
                position: new Cesium.GeometryAttribute({
                    componentDatatype: Cesium.ComponentDatatype.FLOAT,
                    componentsPerAttribute: 3,
                    values: new Float32Array(positions)
                })
            },
            indices: indices,
            boundingSphere: Cesium.BoundingSphere.fromVertices(positions)
        })
        return geometry
    }
// 创建 绘制命令
    createDrawCommand(context) {

        let geometry = this.createAnPyramidGeometry()

        let vertexArray = Cesium.VertexArray.fromGeometry({
            context: context,
            geometry: geometry,
            attributeLocations: Cesium.GeometryPipeline.createAttributeLocations(geometry),
        });

        let shaderProgram = Cesium.ShaderProgram.fromCache({
            context: context,
            vertexShaderSource: testVert,
            fragmentShaderSource: testFrag,
            attributeLocations: Cesium.GeometryPipeline.createAttributeLocations(geometry),
        })
        
        let uniformMap = {}

        let renderState = Cesium.RenderState.fromCache({
            depthTest:{
                flat: true,
            }
        })

        this.drawCommand = new Cesium.DrawCommand({
            vertexArray: vertexArray,
            shaderProgram: shaderProgram,
            uniformMap: uniformMap,
            renderState: renderState,
            pass: Cesium.Pass.OPAQUE
        })

    }

物体的平移

在三维世界里,矩阵是用于表示物体的位置的,有许多的表示位置,诸如物体的模型坐标,投影坐标,相机坐标,我们这里应该操作的是模型坐标,一个带有张力的“弹跳”其实也只不过是一个函数的具体体现,在这个过程里,需要一个确切的值表示物体平移的快慢,我们使用最普遍的最常见的图像cos 即可。

  • 为何使用cos 图像?
    只关注它的几何意义,众所周知,cos 0 等于1, 意味着要改变物体的y轴上的值时,当我们的物体在起跳的瞬间是最快的,在跳到限定值的那一刻是最慢的,这样子,物体的平移就会呈现出这样一种形态:在地面时快速跃起,在高空时缓缓落下,重复这个动作,即完成了这个效果

着色器里做如下操作

attribute vec3 position3DHigh;
attribute vec3 position3DLow;
attribute float batchId;
attribute vec3 position;
void main() {
    float upLimit = 0.3;
    float ty = abs(cos(czm_frameNumber * 0.03)) * upLimit;
    mat4 translateY = mat4(1, 0, 0, 0, 0, 1, 0, ty, 0, 0, 1, 0, 0, 0, 0, 1);
    gl_Position = czm_projection * czm_modelView * vec4(position, 1.0) * translateY;
}

以上就是一个 更新弹跳的函数,float ty 此变量 意味着 原本 是 将 cos 图像 从 0 - 1 =》 0 - 0.3。
有关的知识请去网上搜索,平移矩阵、czm_ 此类变量等等.

物体的旋转

旋转的矩阵种类有不少: 绕xyz轴旋转,绕点旋转,绕任意轴旋转,网上相关文章很多,这里说明此文中的绕自身中心轴旋转,注意:并非绕中心点旋转,这并不是一个概念上的东西。
原理在下图。
在这里插入图片描述
也就是绕物体的中心轴旋转才是我们需要的。并不是绕点,也不是绕xyz轴。

新增补充 来自2022/11/17

本来已经忘记了,结果偶然看到有人询问为啥提供的示例代码里的旋转歪了。群友解决了问题也热心的分享了解决方法他的博客地址

  • 顶点着色器的glsl代码如下
  • v1,v2,theta 分别为 起始点、结束点、旋转角度. 得到此结果乘以原本的位置 即可按任意轴旋转。
  • 在此文所示 v1应为倒四棱锥底部的那个点 以及顶部的面对应的中点,如上图所示。将两点的世界坐标传入即可正确计算。
// 此为
mat4 RodriguesRotation(vec3 v1, vec3 v2, float theta){
            float a = v1.x;
            float b = v1.y;
            float c = v1.z;
        
            vec3 _p = v2 - v1;
            vec3 p = normalize(_p);
            float u = p.x;
            float v = p.y;
            float w = p.z;
        
            float uu = u * u;
            float uv = u * v;
            float uw = u * w;
            float vv = v * v;
            float vw = v * w;
            float ww = w * w;
            float au = a * u;
            float av = a * v;
            float aw = a * w;
            float bu = b * u;
            float bv = b * v;
            float bw = b * w;
            float cu = c * u;
            float cv = c * v;
            float cw = c * w;
        
            float costheta = cos(theta);
            float sintheta = sin(theta);

            float _m00 = uu + (vv + ww) * costheta;
            float _m01 = uv * (1.0 - costheta) + w * sintheta;
            float _m02 = uw * (1.0 - costheta) - v * sintheta;
            float _m03 = 0.0;
        
            float _m10 = uv * (1.0 - costheta) - w * sintheta;
            float _m11 = vv + (uu + ww) * costheta;
            float _m12 = vw * (1.0 - costheta) + u * sintheta;
            float _m13 = 0.0;
        
            float _m20 = uw * (1.0 - costheta) + v * sintheta;
            float _m21 = vw * (1.0 - costheta) - u * sintheta;
            float _m22 = ww + (uu + vv) * costheta;
            float _m23 = 0.0;
        
            float _m30 = (a * (vv + ww) - u * (bv + cw)) * (1.0 - costheta) + (bw - cv) * sintheta;
            float _m31 = (b * (uu + ww) - v * (au + cw)) * (1.0 - costheta) + (cu - aw) * sintheta;
            float _m32 = (c * (uu + vv) - w * (au + bv)) * (1.0 - costheta) + (av - bu) * sintheta;
            float _m33 = 1.0;

            return mat4(_m00,_m01,_m02,_m03,_m10,_m11,_m12,_m13,_m20,_m21,_m22,_m23,_m30,_m31,_m32,_m33);
        }

cesium drawcommand 纹理贴图

使用纹理通常来说主要两个步骤:一般来说两个都得有,但也可以缺省下方的1。
1、将顶点的纹理坐标传入到着色器中
2、将图片源转换成glsl识别的sampler2D数据

看上去2很费劲,其实也差不多,无论是框架还是底层,都已经帮我们处理了很多。

最基础的纹理知识,纹理坐标系的表示。同时将顶点对应至下图。
在这里插入图片描述
当我们对此文中的四棱锥顶点的纹理坐标贴一个对应的图片的时候,他应该会在四棱锥的底部把完整的图片渲染,同时,各个三角形的着色会对应显示。读者可先自行脑补。

先修改geometry的实现,此处我们需要把顶点对应的纹理坐标传入shader中做处理。
注意:顶点位置数组应与纹理坐标数组对应,为何是这个顺序,请参考上面生成四棱锥的步骤。01234.

在createAnPyramidGeometry方法内添加如下代码

let st = [
	0.5,0.5,
	0.0,1.0,
	1.0,1.0,
	0.0,0.0,
	1.0,0.0
]

//  必要明确的告诉 框架 去怎么样 处理 你提供进去的数据.
        let geometry = new Cesium.Geometry({
            //  告诉cesium 你需要怎样读 数组里面的内容
            attributes: {
                position: new Cesium.GeometryAttribute({
                    componentDatatype: Cesium.ComponentDatatype.FLOAT,
                    componentsPerAttribute: 3,
                    values: new Float32Array(positions)
                }),
                st: new Cesium.GeometryAttribute({
                    componentDatatype: Cesium.ComponentDatatype.FLOAT,
                    componentsPerAttribute: 2,
                    values: new Float32Array(st)
                })
            },
            indices: indices,
            boundingSphere: Cesium.BoundingSphere.fromVertices(positions)
        })

在着色器内部声明该变量,同时传递进片元着色器。

attribute vec2 st;
varying vec2 v_st;
void main() {
	// ...
    v_st = st;
}

以上已经完成了此文中阐述的第一点。

之前我们处理了顶点属性的纹理坐标的写入,现在它已经存在于片元着色器中。通过glsl语言内置的texture2D函数,我们可以从一个图片 与 一组纹理坐标的映射对应的读取到该图片位于某一个位置的RGBA值,为此,仅仅处理完纹理坐标不足以进行纹理的贴图,我们需要传递一张图片进着色器。问题来了,glsl并不能识别我们通常意义上的诸如jpg.png等类型的文件,而从本质上来说图片依然是由一个个数据去构成的,为此glsl语言定义了一个sample2D作为xy轴构成的图片的定义(我焯,有点绕口)。即sample2D实际上是通过对原始图片内容处理过的数据。但幸运的是,我们在现在的三维框架里并不需要去处理这些。比如在cesium里,我们只需调用它封装的纹理生成类即可。

	//创建纹理
    createTexture(context){
    	if(this._image == null){
    		this._image = new Image()
        	this._image.src = `/laopo.png`
        	let that = this
        	this._image.onload = function(img){
            let vTexture = new Cesium.Texture({
                context: context,
                source: image
            });
            that._texture = vTexture
        }
    	}
    }

顺带一提,为何不直接声明let image,原因在于update函数内做得判断,图片资源加载时一个异步过程,当_texture未被赋值时,此函数会被重复执行,为此需要处理一下。与下方uniformMap 部分的代码的逻辑是一样的。
在update函数内

	update(frameState) {
		// ...
        if(this._texture === null){
            this.createTexture(frameState.context)
        }
		// ...
	}

在createDrawCommand方法内写入

let uniformMap = {
	wenli: ()=>{
		if(Cesium.defined(this._texture)){
			return this._texture;
		} else {
			return context.defaultTexture;
		}
	}
}
  • 为何仍需要做判断?

原因在于请求图片也好,将普通图片转换成sample2D也罢,都是个异步的过程,在此期间,你的着色器内部可能已经开始工作,而你的texture始终没到,当你写在着色器的代码有对纹理进行采样的时候,会导致webgl的内部处理错误。因此,如果纹理处理好了,返回纹理,没处理好,返回cesium默认的纹理,保证不出错即可。

  • 如何使用纹理?
    在fragShader(片元着色器) 下调用以下代码
varying vec2 v_st; //纹理坐标 一般是从顶点着色器中传递过来
uniform sampler2D wenli; // 声明sampler2D 的纹理数据常量
void main(){
	// texture2D函数 意味着 在v_st的坐标上 对 wenli 图片进行采样(白话就是:读该位置的rgba值 同时返回)
	gl_FragColor = texture2D(wenli,v_st);
}

如此,我们就已经完成了纹理的贴图。

光带效果

前置函数内容:

1、mix(genType x,genType y,float a)
2、smoothstep(edge0,edge1, float a)

下面分别解析函数的具体意义。

mix 一个线性插值函数,以最为简单的二维举例(其余自己类比)具体的表现为
在这里插入图片描述
其中a 为比例,取值范围在 0 - 1。

主要的作用在于通过一段确定的直线,根据一个确定的比例,获取在这条线段上的相对应的值。

smoothstep 是一个平滑阶梯函数,具体的表现为
在这里插入图片描述
即小于edge0 返回0,大于edge1 返回1, 在中间,则是一个 平滑插值(非线性插值)。

介绍完这两函数之后 回到正题,我们思考一下一个光带扫描的效果。从朴素的想法出发,归类为以下几点。

1、光带的形成
2、光带的移动
3、颜色的混合

这三个问题看似是割裂的,但实际上它是一个整体的设计,即其中是存在耦合的并不是单独的功能组合,这也导致一点,如果需要一个一个的阐述清楚,势必会造成逻辑上的割裂感。我试图去从整体上为大家说明。

先从 以X轴扫过的光带作为举例,幻想下有一条 在webgl 的坐标上为 0.2 - 0.1 的宽度长条 徐徐前进。
在这里插入图片描述
上面说到过mix与smoothstep,下文我就默认这个已经成为常识。此时我们把让这个框内的颜色全部置成白色,同时让它随着时间逐渐的向右向左 在0 - 1之间反复运动如何操作?

这个就很简单,我们只需在着色器内部这样定义

vec2 uv = gl_FragCoord.xy / iResolution.xy;
float d = uv.x;
r = abs(sin(r * czm_frameNumber));
//...
if(d >= r && d <= r + 0.1){
   gl_FragColor = vec4(1.0)
}

d跟r 其实带入以下就很好理解了,r 意味着光带的edge0,r+0.1 即上图中的 0.2 edge1。 d 则是当前屏幕输出的坐标。

gl_FragCoord 表示当前片元着色器处理的候选片元窗口相对坐标信息,是一个 vec4 类型的变量。也就是说,假设我们屏幕的分辨率为1920*1080,它最右上角的坐标位置就是(1920,1080),意味着,它此时并不是一个webgl坐标系内的坐标,我们需要转换成webgl的空间坐标,所以需除去当前容器所占的分辨率(也就是宽高)。

这样子,我们就得到了一个移动的光带,但是得说这个光带并不好看,而且有些粗暴,它就是把那一列中的所有颜色单纯的置成了白色,这样子其实从视觉效果上来说,并不行。尽管它仍然能实现。这种类似扫描的效果。

为此,我们需要一个平滑的函数去控制这个框内显示的颜色,可以有很多种过渡,这里讲一种最直观的一种过度。假设将上图白框分为3份即中间为纯色,左右两边是一种过渡的渐变色。

此时上面介绍的smoothstep就派上用场了,sorry 我已经很努力得尝试将它放正了。

在这里插入图片描述
修改代码

// ...
float c = smoothstep(r, r+0.04, d) - smoothstep(r + 0.08,r + 0.12, d);
if(d >= r && d <= r + 0.12){
  gl_FragColor = vec4(c)
}

在这里插入图片描述
光柱变成了这副模样,跟我们预期得一致,等等,这好像看起来也怪怪的啊,没错,要让这个光柱显示更为平滑点,我们还需要使用一个mix函数,对它的左值右值限定。

vec4 guangdaiColor = vec4(mix(vec4(1.0),vec4(c),c));

此时颜色如下图形状
在这里插入图片描述
接着在跟我们原始的颜色进行mix操作,会惊奇的发现变成了一个发散的淡灰色。注意,这部分的mix的左右是不可颠倒的,试着带入数字体会着里面的输出。实际上当c 这个参数为0的时候,也就意味着此时我们着色器中输出的应该是物体原本的颜色值,而当它开始进入范围内时,c开始不为0的时候,输出在原始颜色值 与光带的颜色值 的一个线性插值。

gl_FragColor = mix(color,guangdaiColor,c);

在这里插入图片描述
OK,那么其实如果你已经全然熟悉原理了,其实对一些智慧城市的扫描效果,心里应该也是有数了。假设我们现在不想要白光了要如何操作呢?mix一下就完事了。例如。

在这里插入图片描述

vec4 originColor = vec4(0,245.0,255.0,1.0);
vec4 guangdaiColor = vec4(mix(vec4(c),originColor,c));

不要嫌弃中间色块太大,明白原理之后,你应该知道只需稍稍的改一下 r+0.04 r+0.08的距离就可以了。

y轴方向上的跟x轴方向上的光带扫描差不多,这里就不赘述了。这里稍微提几句圆弧的。

主要的处理在于length 这个函数

  • length
    求一个向量的长度
    在这里插入图片描述
    虽稍显粗糙,但相信你们能秒懂。代码就改动下面这部分即可。下方有个乘等的操作,具体的理由呢则是,通常一个容器的分辨率都是宽比高要长,如果我们不对uv这个变量做任何的处理,它应是呈一个椭圆的扩散。
vec2 uv = gl_FragCoord.xy / iResolution.xy;
uv.x *= iResolution.x/iResolution.y;
float d = length(uv);

也就是说 x轴属于是被拉伸的,我们需要乘一个跟宽高有关的比例让它计算的位置对应。由于我们的length(uv) 会超出webgl的渲染范围(比如uv此时的坐标为(1,1), 因此 也会导致一种情况的出现,即x轴方向上的光带扫描未必能完整的覆盖物体。

  • 解决方法
    拉伸光带的识别范围
r = abs(sin(r * czm_frameNumber) * iResolution.x/iResolution.y);

也有个具体的小示例供大家观看
https://www.shadertoy.com/view/Nt2fDV

视频地址

公开免费,文章与视频的代码可随意使用
https://space.bilibili.com/298961070

结语

不知不觉这个博客已经码了9000字。俺人都麻了。不过好像也还行把,至少完整系统的讲明白了,写博客有一个好玩的地方就是你不确定有人看到这篇文章会是什么时候,这时候发现你想走的路原来已经有人走过,这是一件有意义的事情。走慢走快都是走,希望在这条路上能得到支持鼓励吧。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐