Cesium在1.46版本中新增了对整个场景的后期处理(Post Processing)功能,包括模型描边、黑白图、明亮度调整、夜视效果、环境光遮蔽等。对于这么炫酷的功能,我们绝不犹豫,先去翻一翻它的源码,掌握它的实现原理。

1 后期处理的原理

  后期处理的过程有点类似于照片的PS。生活中拍摄了一张自拍照,看到照片后发现它太暗了,于是我们增加亮度得到了一张新的照片。在增加亮度后发现脸上的痘痘清晰可见,这可不是我们希望的效果,于是再进行一次美肤效果处理。在这之后可能还会进行n次别的操作,直到满足我们的要求。上述这个过程和三维里面的后期处理流程非常类似:拍的原始照片相当于三维场景中实际渲染得到的效果,在此基础上进行物体描边、夜视效果、环境光遮蔽等后期处理,最后渲染到场景中的图片相当于定版的最终照片。整个过程如下图所示:

2 Cesium添加后期处理的流程

  在介绍Cesium添加后期处理流程之前,首先对用到的相关类进行说明:

PostProcessStage:对应于某个具体的后期处理效果,它的输入为场景渲染图或者上一个后期处理的结果图,输出结果是一张处理后的图片。

PostProcessStageComposite:一个集合对象,存储类型为PostProcessStage或者PostProcessStageComposite的元素。

PostProcessStageLibrary:负责创建具体的后期处理效果,包括Silhouette、Bloom、AmbientOcclusion等,创建返回的结果是PostProcessStageComposite或者PostProcessStage类型。

PostProcessStageCollection:是一个集合类型的类,负责管理和维护放到集合中的元素 ,元素的类型是PostProcessStage或者PostProcessStageComposite。

  Cesium中添加后期处理的流程是:首先通过PostProcessStageLibrary创建一个或者多个后处理效果对象,得到多个PostProcessStage或者PostProcessStageComposite,然后将他们加入到PostProcessStageCollection对象中。这样PostProcessStageCollection对象就会按照加入的顺序进行屏幕后期处理,在所有的效果都处理完毕后,执行FXAA,最后绘制到屏幕上。下面对Silhouette实现原理进行介绍,Ambient Occlusion实现原理将会在下一篇文章中单独进行说明。

3 Silhouette实现原理

3.1 开启物体描边功能

  silhouette的效果可以理解为物体轮廓、描边,相当于把物体的外轮廓线勾勒出来。在Cesium中开启silhouette的代码和效果如下:

1 var collection = viewer.scene.postProcessStages;
2 var silhouette = collection.add(Cesium.PostProcessStageLibrary.createSilhouetteStage());
3 silhouette.enabled = true;
4 silhouette.uniforms.color = Cesium.Color.YELLOW;

3.2 js代码内容

  创建Stage的函数是实现功能的关键所在,Cesium.PostProcessStageLibrary.createSilhouetteStage()这个函数的具体内容如下:

 1 PostProcessStageLibrary.createSilhouetteStage = function() {
2 var silhouetteDepth = new PostProcessStage({
3 name : 'czm_silhouette_depth',
4 fragmentShader : LinearDepth
5 });
6 var edgeDetection = new PostProcessStage({
7 name : 'czm_silhouette_edge_detection',
8 fragmentShader : EdgeDetection,
9 uniforms : {
10 length : 0.25,
11 color : Color.clone(Color.BLACK)
12 }
13 });
14 var silhouetteGenerateProcess = new PostProcessStageComposite({
15 name : 'czm_silhouette_generate',
16 stages : [silhouetteDepth, edgeDetection]
17 });
18 var silhouetteProcess = new PostProcessStage({
19 name : 'czm_silhouette_color_edges',
20 fragmentShader : Silhouette,
21 uniforms : {
22 silhouetteTexture : silhouetteGenerateProcess.name
23 }
24 });
25
26 var uniforms = {};
27 defineProperties(uniforms, {
28 length : {
29 get : function() {
30 return edgeDetection.uniforms.length;
31 },
32 set : function(value) {
33 edgeDetection.uniforms.length = value;
34 }
35 },
36 color : {
37 get : function() {
38 return edgeDetection.uniforms.color;
39 },
40 set : function(value) {
41 edgeDetection.uniforms.color = value;
42 }
43 }
44 });
45 return new PostProcessStageComposite({
46 name : 'czm_silhouette',
47 stages : [silhouetteGenerateProcess, silhouetteProcess],
48 inputPreviousStageTexture : false,
49 uniforms : uniforms
50 });
51 };

  通过浏览代码发现,该函数最后的返回结果是PostProcessStageComposite对象,该对象包含了silhouetteGenerateProcess和silhouetteGenerateProcess两个元素,其中silhouetteGenerateProcess又是一个PostProcessStageComposite类型,包括silhouetteDepth和edgeDetection两部分。在后期处理过程中真正起作用的是PostProcessStage类型的对象,此处包括silhouetteDepth、silhouetteDepth、silhouetteProcess三个对象,也就是说这三个对象的顺序执行实现了物体描边效果。对于PostProcessStage这种类型的对象,它的输入值包括一些效果参数和一张输入照片,顶点着色器没有什么特殊内容,就是构建一个贴屏幕的四边形,重点全部在片源着色器中。下面对这三个片源着色器中的代码进行详细分析。

3.3 LinearDepth

  LinearDepth的代码如下:

 1 uniform sampler2D depthTexture;
2
3 varying vec2 v_textureCoordinates;
4
5 float linearDepth(float depth)
6 {
7 float far = czm_currentFrustum.y;
8 float near = czm_currentFrustum.x;
9 return (2.0 * near) / (far + near - depth * (far - near));
10 }
11
12 void main(void)
13 {
14 float depth = czm_readDepth(depthTexture, v_textureCoordinates);
15 gl_FragColor = vec4(linearDepth(depth));
16 }

  代码比较简单,一共才10多行,目的就是将深度图中的深度值进行线性拉伸。depthTexture代表场景中的深度图,v_textureCoordinates代表屏幕采样点坐标。首先通过czm_readDepth读取场景中的深度值,然后利用linearDepth函数(该函数通过远近裁剪面对输入值做了一个线性变换)进行线性拉伸。其实质是把深度值转换成视空间下的z值,然后将这个z值除以far,得到一个0-1的值,该值的大小可以反应屏幕像素点在视空间下的z值大小。最后将得到的深度值赋值给gl_FragColor变量,相当于把深度值隐藏在颜色中。这样就得到了一张经过线性拉伸后的深度图,用于后面的处理。

3.4 EdgeDetection

  EdgeDetection的代码如下:

 1 uniform sampler2D depthTexture;
2 uniform float length;
3 uniform vec4 color;
4
5 varying vec2 v_textureCoordinates;
6
7 void main(void)
8 {
9 float directions[3];
10 directions[0] = -1.0;
11 directions[1] = 0.0;
12 directions[2] = 1.0;
13
14 float scalars[3];
15 scalars[0] = 3.0;
16 scalars[1] = 10.0;
17 scalars[2] = 3.0;
18
19 float padx = 1.0 / czm_viewport.z;
20 float pady = 1.0 / czm_viewport.w;
21
22 float horizEdge = 0.0;
23 float vertEdge = 0.0;
24
25 for (int i = 0; i < 3; ++i) {
26 float dir = directions[i];
27 float scale = scalars[i];
28
29 horizEdge -= texture2D(depthTexture, v_textureCoordinates + vec2(-padx, dir * pady)).x * scale;
30 horizEdge += texture2D(depthTexture, v_textureCoordinates + vec2(padx, dir * pady)).x * scale;
31
32 vertEdge -= texture2D(depthTexture, v_textureCoordinates + vec2(dir * padx, -pady)).x * scale;
33 vertEdge += texture2D(depthTexture, v_textureCoordinates + vec2(dir * padx, pady)).x * scale;
34 }
35
36 float len = sqrt(horizEdge * horizEdge + vertEdge * vertEdge);
37 float alpha = len > length ? 1.0 : 0.0;
38 gl_FragColor = vec4(color.rgb, alpha);
39 }

  通过shader的名字就可以大体猜到这段代码的作用就是对边界进行检测。depthTexture是通过linearDepth拉伸后的深度图,length是设置的物体边界长度判断值,color是设置的边界颜色,v_textureCoordinates是屏幕采样点的坐标。在main函数中首先定义了directions和scalars两个数组。directions代表进行边界检测的方向,scalars表示边界检测的权重值。padx表示每个像素在x方向上的坐标跨度,pady表示每个像素在y方向上的坐标跨度。horizEdge表示水平方向的边界值,vertEdge表示竖直方向边界值。然后就是通过for循环在以该像素为中心的九宫格中计算水平方向的深度差值和垂直方向的深度差值,计算的过程可以用下图表示:

  通过上面这张图可以清晰的看出,边界检测的过程其实是对周围八个像素点计算z坐标差值,包括水平坐标差值horizEdge和竖直差值vertEdge。通过这两个值得到总差值len,通过表和len的 大小设置颜色的透明度为1或者0,输出一张图。

3.5 Silhouette

  Silhouette的代码如下:

 1 uniform sampler2D colorTexture;
2 uniform sampler2D silhouetteTexture;
3
4 varying vec2 v_textureCoordinates;
5
6 void main(void)
7 {
8 vec4 silhouetteColor = texture2D(silhouetteTexture, v_textureCoordinates);
9 gl_FragColor = mix(texture2D(colorTexture, v_textureCoordinates), silhouetteColor, silhouetteColor.a);
10 }

  silhouette的代码非常简单,其中colorTexture代表原始场景图,silhouetteTexture是通过EdgeDetection得到的图。通过silhouetteColor.a进行两张图的混合,就可以得到最终的结果。

4 总结

  后期处理其实是一个叠加修改的过程,通过不同步骤的加工,最后得到想要的结果。本文所讲的物体描边其实是对整个屏幕中的要素进行边界检测,检测出为边界的地方就将其颜色改为设定的值。花了大半天时间写完了,希望对感兴趣的同学有所帮助。晚上我要出去玩,玩,玩!!!

PS:Cesium交流可以扫码加群,期待你的加入!!!

Cesium源码剖析---Post Processing之物体描边(Silhouette)的更多相关文章

  1. Cesium源码剖析---Clipping Plane

    之前就一直有写博客的想法,别人也建议写一写,但一直没有动手写,自己想了一下原因,就一个字:懒.懒.懒.为了改掉这个毛病,决定从今天开始写博客了,一方面对自己掌握的知识做一个梳理,另一方面和大家做一个交 ...

  2. Cesium源码剖析---视频投影

    Cesium中的视频投影是指将视频作为一种物体材质,实现在物体上播放视频的效果.这个功能在Cesium早期版本中就支持了,在Code Example中有一个示例.今天就来分析一下其内部实现原理. 1. ...

  3. Cesium源码剖析---Ambient Occlusion(环境光遮蔽)

    Ambient Occlusion简称AO,中文没有太确定的叫法,一般译作环境光遮蔽.百度百科上对AO的解释是这样的:AO是来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果,可以解决或改善漏光 ...

  4. SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现

    SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...

  5. 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析

    通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...

  6. Netty学习笔记(三)——netty源码剖析

    1.Netty启动源码剖析 启动类: public class NettyNioServer { public static void main(String[] args) throws Excep ...

  7. SpringMVC源码剖析1——执行流程

    SpringMVC源码剖析1——执行流程 00.SpringMVC执行流程file:///C:/Users/WANGGA~1/AppData/Local/Temp/enhtmlclip/Image.p ...

  8. jQuery之Deferred源码剖析

    一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...

  9. Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

    声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...

随机推荐

  1. java 编程基础 Class对象 反射 :参数反射

    方法参数反射 Java8在java.lang.reflect包下新增了Executable抽象基类,该对象代表可执行的类成员,该类派生了Constructor和Method两个子类.Executabl ...

  2. java 多线程:线程通信-等待通知机制wait和notify方法;(同步代码块synchronized和while循环相互嵌套的差异);管道通信:PipedInputStream;PipedOutputStream;PipedWriter; PipedReader

    1.等待通知机制: 等待通知机制的原理和厨师与服务员的关系很相似: 1,厨师做完一道菜的时间不确定,所以厨师将菜品放到"菜品传递台"上的时间不确定 2,服务员什么时候可以取到菜,必 ...

  3. SpringBoot 设置请求字符串格式为UTF-8

    增加一个过滤器 package com.config; import com.jetsum.business.common.constant.CharsetConstant; import lombo ...

  4. ubuntu(Linux) c++ 获取本机IPv4和ipv6、查询本机IPv4,IPv6

    1.关于 演示环境: Linux xxxxxxx 5.4.0-47-generic #51-Ubuntu SMP Fri Sep 4 19:50:52 UTC 2020 x86_64 x86_64 x ...

  5. 【LeetCode】957. Prison Cells After N Days 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 周期是14 日期 题目地址:https://leet ...

  6. Problem 2233 ~APTX4869

    Problem 2233 ~APTX4869 Accept: 55    Submit: 176Time Limit: 1000 mSec    Memory Limit : 32768 KB Pro ...

  7. 浏览器Cookie无法删除问题

    1.写Cookie时添加路径: //写cookies,设置过期时间为30天,并设置path为根目录 function setPathCookie(name,value) { var Days = 30 ...

  8. ImageNet2017文件下载

    ImageNet2017文件下载 文件说明 imagenet_object_localization.tar.gz包含训练集和验证集的图像数据和地面实况,以及测试集的图像数据. 图像注释以PASCAL ...

  9. Error: Cannot find module '@dcloudio/uni-cli-i18n' 解决方案

    这个错误是因为node_modules缺少了   '@dcloudio/uni-cli-i18n' 以下是错误信息  解决方案: yarn add -D @dcloudio/uni-cli-i18n ...

  10. Office - 0x4004F00C解决方法

    之前用HEU_KMS激活了180天的office 2013(专业版),今天打开突然报错0x4004F00C 大致意思就是提示你:office还有几天将要过期,到时候只能查看文档.表格.ppt,而不能编 ...