cesium 旋转弹跳四棱锥光带扫描动态泛光效果

前言:本文主要分以下几个部分阐述功能的核心实现。
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轴。

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340