[
第一部分 ]
在处理了基础知识之后,在本文的这一部分中,我们实现了诸如对象轮廓,光晕,SSAO,模糊,景深,像素化等效果。
概述
在场景的几何图形周围创建轮廓,使游戏具有类似于漫画或卡通的独特外观。
扩散材料
轮廓着色器需要输入纹理来识别和着色边缘。 此类传入纹理的候选对象可以是材质的漫反射颜色,漫反射纹理的颜色,顶点法线,甚至可以是法线贴图的颜色。
uniform struct { vec4 diffuse ; } p3d_Material; out vec4 fragColor; void main() { vec3 diffuseColor = p3d_Material.diffuse.rgb; fragColor = vec4(diffuseColor, 1); }
这是一个小的片段着色器,可将几何材料的漫反射颜色渲染到帧缓冲区纹理中。 来自帧缓冲区的漫反射色纹理将成为路径着色器的输入纹理。
这是帧缓冲区中材质的漫反射颜色的纹理,该纹理显示了我们在Blender中设置的颜色。 轮廓着色器将识别场景中的边缘并为其着色。
应该注意的是,如果场景的某些部分没有它们自己的材质的漫反射色,则材质的漫反射色将不起作用。
创建边缘
创建边缘类似于在
GIMP中使用边缘识别过滤器。
此着色技术的所有计算都在片段着色器中执行。 要为顶点着色器创建轮廓,只需将矩形网格的四个顶点传递到输出以适合屏幕即可。
在开始识别边缘之前,您需要准备传入的纹理,我们将使用该纹理。 由于纹理具有屏幕大小,因此我们可以计算UV坐标,知道片段的坐标和传入纹理的大小。
可以根据您的口味定制
separation
。 间隔越大,边缘或线条越粗。
边缘识别技术可发现传入纹理的颜色变化。 着眼于当前片段,它使用3x3片段窗口查找九个样本中最亮和最暗的颜色。 然后,她从一种颜色的亮度中减去另一种颜色的亮度,得到它们的差值。
此差异用于输出颜色的Alpha通道。 如果没有差异,则不会绘制边缘或线条。 如果存在差异,则绘制边缘。
尝试试验阈值。 现在为零。 任何非零值都将成为边;可以更改此阈值。 这对于差异较小的噪声较大的传入纹理特别有用。 对于嘈杂的传入纹理,通常只需要为较大的差异创建轮廓。
源代码
雾气
雾(或薄雾,在Blender中被称为雾)在场景中增添了大气的阴霾,创造出神秘而柔软的突出部分。 当某些几何形状突然落入相机可见性金字塔时,会出现突出部分。
Panda3D具有方便的数据结构,其中包含所有雾参数,但是您可以将其手动传输到着色器。
在该代码示例中,线性模型用于计算从相机移开时雾的亮度。 相反,您可以使用指数模型。 雾的亮度在雾开始之前或开始时为零。 当顶点位置接近雾的末端时,雾
fogIntensity
接近统一。 对于雾结束后的所有顶点,从上方将雾
fogIntensity
限制为1。
根据雾的亮度,我们将雾的颜色与输出颜色混合。 随着
fogIntensity
趋于一致,将会有越来越少的
outputColor
和越来越多的雾色。 当
fogIntensity
达到单位时,将仅保留雾的颜色。
轮廓上的雾
路径着色器将雾化到边缘的颜色,以获得更整体的图像。 如果他不这样做,轮廓的几何形状将被雾遮盖,这看起来很奇怪。 但是,他仍然使用铣刀在舞台几何图形的最外边缘上创建轮廓,因为这些边缘超出了几何图形-到达了没有顶点位置的位置。
positionTexture
是一个帧缓冲区纹理,其中包含视图空间的顶点位置。 当我们实现SSAO着色器时,您将了解到这一点。
源代码
布卢姆
向场景中添加花开可以使照明模型产生令人信服的幻觉。 发光物体变得更有说服力,并且光反射会接收到更多的辐射。
您可以根据自己的喜好自定义这些设置。 分离会增加模糊的大小。 样本确定模糊的强度。 阈值确定此影响将影响什么和将不会受到什么影响。 数量控制光晕输出的量。
该技术首先将窗口大小的
samples
传递到相对于当前片段居中的
samples
上。 它看起来像一个用于创建路径的窗口。
此代码从传入的纹理获取颜色,并将红色,绿色和蓝色的值转换为灰度值。 如果灰度值小于阈值,则它将丢弃此颜色,使其变为黑色。
通过窗口中的所有样本,它将所有
result
值累加。
完成样本收集后,他将颜色样本的总和除以所采样的数目。 结果是片段本身及其邻居的中间颜色。 通过对每个片段执行此操作,我们得到了模糊的图像。 这种模糊称为框模糊。
在这里,您将看到执行Bloom算法的过程。
源代码
屏幕空间环境光遮蔽(SSAO)
SSAO是您不知道存在的那些影响之一,但是一旦您知道它们就无法生存。 他可以将平庸的场景变成令人惊叹的场景! 在静态场景中,可以将环境光遮挡烘焙到纹理中,但是对于更多动态场景,我们需要一个着色器。 SSAO是更复杂的着色技术之一,但是一旦弄清楚了,您将成为高级着色器。
请注意,标题中的“屏幕空间”一词并不完全正确,因为并非所有计算都在屏幕空间中执行。
传入数据
SSAO着色器将需要以下输入。
- 可视空间中顶点位置的向量。
- 视空间中顶点的法线向量。
- 切线空间中的样本向量。
- 切线空间中的噪声向量。
- 相机镜头上的投影矩阵。
位置
不必将顶点位置存储在帧缓冲区纹理中。 我们可以从
相机深度缓冲区重新创建它们。 我正在为初学者编写指南,因此我们将不会使用此优化,而是立即着手开展业务。 在您的实现中,您可以轻松使用深度缓冲区。
PT(Texture) depthTexture = new Texture("depthTexture"); depthTexture->set_format(Texture::Format::F_depth_component32); PT(GraphicsOutput) depthBuffer = graphicsOutput->make_texture_buffer("depthBuffer", 0, 0, depthTexture); depthBuffer->set_clear_color(LVecBase4f(0, 0, 0, 0)); NodePath depthCameraNP = window->make_camera(); DCAST(Camera, depthCameraNP.node())->set_lens(window->get_camera(0)->get_lens()); PT(DisplayRegion) depthBufferRegion = depthBuffer->make_display_region(0, 1, 0, 1); depthBufferRegion->set_camera(depthCameraNP);
如果决定使用深度缓冲区,则可以在Panda3D中配置深度缓冲区。
in vec4 vertexPosition; out vec4 fragColor; void main() { fragColor = vertexPosition; }
这是一个简单的着色器,用于将查看空间中的顶点位置渲染为帧缓冲区纹理。 更加困难的任务是调整帧缓冲区的纹理,以使由此获得的片段向量的分量不限于间隔
[0, 1]
,并且每个片段具有足够高的精度(足够多的位数)。 例如,如果某种插值的顶点位置是
<-139.444444566, 0.00000034343, 2.5>
,则无法将其保存为
<-139.444444566, 0.00000034343, 2.5>
到纹理。
这是一个示例代码,该代码准备用于存储顶点位置的帧缓冲区纹理。 他需要32位用于红色,绿色,蓝色和Alpha,因此他将值的限制禁用为
[0, 1]
间隔。 对
set_rgba_bits(32, 32, 32, 32)
的调用将设置位音量并禁用限制。
glTexImage2D ( GL_TEXTURE_2D , 0 , GL_RGB32F , 1200 , 900 , 0 , GL_RGB , GL_FLOAT , nullptr );
这是在OpenGL上的类似调用。
GL_RGB32F
设置位并禁用限制。
如果颜色缓冲区具有固定的逗号,则对于无符号归一化和有符号归一化颜色缓冲区,在计算混合方程之前,初始值和最终值的成分以及混合指数分别限制为[0,1]或[-1,1]。 如果颜色缓冲区具有浮点,则不符合限制。
来源
在这里,您可以看到顶点的位置; y轴朝上。
请记住,Panda3D将z轴定义为指向上的向量,而在OpenGL中,y轴向上看。 位置着色器以z向上显示顶点的位置,因为在Panda3D中
gl-coordinate-system default
参数已配置。
正常的
为了使在SSAO着色器中获得的样本正确定向,我们需要顶点的法线。 该示例代码生成了分布在半球上的多个采样矢量,但是您可以使用该球体并完全解决需要法线的问题。
in vec3 vertexNormal; out vec4 fragColor; void main() { vec3 normal = normalize(vertexNormal); fragColor = vec4(normal, 1); }
与位置着色器一样,普通着色器非常简单。 记住要对顶点的法线进行归一化,并记住它们在视域中。
此处显示顶点的法线。 y轴朝上。
回想一下,Panda3D将z轴视为向上向量,将OpenGL视为y轴。 由于在Panda3D中配置了
gl-coordinate-system default
,因此普通着色器会显示z轴指向上方的顶点的位置。
样品
为了确定任何单个片段的环境光遮挡值,我们需要对周围区域进行采样。
样本代码生成分布在半球中的64个随机样本。 这些
ssaoSamples
将传递到SSAO着色器。
LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 ).normalized();
如果要在一个球体上分布样本,请更改随机分量z的间隔,以使其从负一变为一。
噪音
为了很好地覆盖采样区域,我们需要生成噪声向量。 这些噪声矢量可使样品围绕表面顶部旋转。
环境遮挡
SSAO通过采样片段周围的可视空间来完成其任务。 表面以下的样品越多,片段的颜色越深。 这些样本位于片段中,并指示顶点法线的大致方向。 每个样本用于在帧缓冲区位置的纹理中搜索一个位置。 将返回的位置与样本进行比较。 如果样品离摄像机的位置比该位置远,则样品被挡住。
在这里,您可以看到采样表面上方的空间以进行遮挡。
像其他一些技术一样,SSAO着色器具有几个控制参数,可以更改这些控制参数以获得所需的外观。 偏差会加到样品到相机的距离上。 此参数可用于消除污渍。 半径增加或减少样本空间的覆盖区域。 lowerRange和upperRange将因子度量标准范围从
[0, 1]
更改为您选择的任何值。 通过增加范围,可以增加对比度。
我们获得位置,法线和随机向量以备将来使用。 回想一下,在代码示例中,创建了16个随机向量。 根据当前片段的屏幕位置选择一个随机向量。
使用随机向量和法线向量,我们收集切线,双法线和法线的矩阵。 我们需要这个矩阵将样本向量从切线空间转换到测量空间。
有了一个矩阵,着色器可以循环遍历循环中的所有样本,减去未打开的样本数。
使用矩阵,将样品放置在顶点/片段位置旁边,并按半径缩放比例。
使用样本在观察空间中的位置,我们将其从观察空间转换为裁剪空间,然后转换为UV空间。
-1 * 0.5 + 0.5 = 0 1 * 0.5 + 0.5 = 1
不要忘记剪切空间的分量在负一到一的范围内,UV坐标在零到一的范围内。 要将剪贴空间的坐标转换为UV坐标,请将它们乘以一秒钟,然后加一秒。
使用通过将3D样本投影到2D位置纹理上获得的UV偏移坐标,我们找到相应的位置矢量。 这使我们从观察空间到裁剪空间再到UV空间,然后又回到观察空间。 着色器运行此循环以确定样品后面,样品位置或样品前面是否存在任何几何形状。 如果样本位于几何图形的前面或某个几何图形中,则相对于重叠片段,根本就不会考虑该样本。 如果样本在某些几何形状后面,则相对于重叠片段考虑该样本。
现在,根据其在半径内或半径外的距离向此采样位置添加权重。 然后,从阻塞度量中减去此样本,因为它假定所有样本在循环之前都已重叠。
将重叠数除以样本数,将遮挡指示器从间隔
[0, NUM_SAMPLES]
转换为间隔
[0, 1]
。 零表示完全遮挡,单位表示没有遮挡。 现在,将遮挡指标分配给片段颜色,仅此而已。
请注意,在示例代码中,为Alpha通道分配了来自帧缓冲区的位置纹理的Alpha值,以避免背景重叠。
模糊
SSAO帧缓冲区的纹理有点嘈杂,因此应对其进行模糊处理以使其平滑。
SSAO模糊着色器是常规的框模糊。 像Bloom着色器一样,它在传入的纹理上绘制一个窗口,并将每个片段及其相邻元素的值平均。
请注意,
parameters.x
是分隔参数。
环境颜色
SSAO面临的最后挑战还是照明计算。 在这里,我们了解了如何在纹理SSAO纹理缓冲区中找到遮挡并将其包含在环境光的计算中。
源代码
景深
景深也是这样一种影响,如果了解了哪些,没有它就无法生存。 从艺术的角度来看,您可以使用它来吸引观看者对特定对象的关注。 但是在一般情况下,以一点点努力为代价的景深会增加很大一部分真实感。
聚焦
第一步是使场景完全聚焦。 渲染到帧缓冲区纹理。 这将是景深缓冲区的输入值之一。
失去重点
第二步是使场景模糊,好像它完全不在焦点上一样。 与Bloom和SSAO一样,您可以使用盒子模糊。 将此散焦场景渲染为帧缓冲区纹理。 这将是景深着色器的另一个输入值。
请注意,
parameters.x
是分隔参数。
混乱
您可以根据自己的喜好自定义这些选项。
focalLengthSharpness
影响场景在焦距下的散焦效果。
focalLengthSharpness
越小,场景在焦距处的散焦就越多。 当远离焦距时,
blurRate
影响场景模糊的速度。
blurRate
,远离焦点时场景的模糊越少。
我们将需要焦点和散焦图像中的颜色。
我们可能还需要在查看空间中顶点的位置。 您可以从用于SSAO的帧缓冲区中重新应用位置的纹理。
在这里发生了混乱。越接近blur
1,它将使用的越多outOfFocusColor
。零值blur
表示此片段完全在焦点上。有了blur >= 1
这个片段就完全散焦了。源代码
后发化
后幅化或颜色采样是减少图像中唯一颜色数量的过程。您可以使用此着色器使游戏具有漫画或复古外观。如果将其与轮廓线结合使用,则会获得真正的卡通风格。
您可以尝试使用此参数。它越大,结果将保留的花越多。
我们将需要传入的颜色。
我还没有看到这样的后代化方法。对其进行检查后,我发现与传统方法相比,它可以产生更漂亮的结果。要减少调色板,请先将颜色转换为灰度值。我们通过将颜色绑定到某一级别来离散化颜色。我们计算灰度的离散值与灰度的非离散值之间的差。将此差异添加到输入颜色。这种差异是颜色必须增加/减少以达到灰度离散值的数量。
不要忘记将输入颜色的值分配给片段的颜色。Cel底纹
后处理可以使图片看起来像cel阴影,因为cel阴影是将漫反射和漫反射颜色离散为离散阴影的过程。我们只想使用纯色漫反射颜色,而没有法线贴图的精细细节和较小的值levels
。源代码
像素化
3D游戏的像素化可以赋予它有趣的外观,或者可以节省您手动创建所有像素图所需的时间。将其与后代化相结合,可以创建出真正的复古外观。
您可以自己调整像素大小。它越大,图像越粗糙。
此技术将每个片段附加到其最近的不重叠像素大小的窗口的中心。这些窗口在传入纹理的顶部对齐。窗口中心的片段确定了其窗口中其他片段的颜色。
确定所需片段的坐标后,从传入纹理中获取其颜色并将其分配给片段颜色。源代码
锐化
锐化效果(锐化)可提高图像边缘的对比度。当图形变得太软时,它会派上用场。
通过更改值,我们可以控制结果的清晰度。如果值为零,则图像不会改变。值为负值时,图像开始看起来很奇怪。
相邻的片段乘以amount * -1
。当前片段乘以amount * 4 + 1
。
相邻的片段位于顶部,底部,左侧和右侧。将邻居和当前片段乘以它们的值后,将结果相加。
此数量是片段的最终颜色。源代码
胶片颗粒
薄膜的颗粒感(小剂量,而不是示例中的情况)可以增加真实感,直到消除此效果之前,该效果才可见。通常,这些缺陷使数字生成的图像更具说服力。请注意,胶片颗粒通常是在显示之前应用于帧的最后一个效果。价值
amount
控制胶片颗粒的可见性。值越高,图片中的“雪”越多。随机亮度
这段代码计算调整该值所需的随机亮度。 Time Since F1 = 00 01 02 03 04 05 06 07 08 09 10 Frame Number = F1 F3 F4 F5 F6 osg_FrameTime = 00 02 04 07 08
Panda3D osg_FrameTime
提供的值。帧时间是一个时间戳,其中包含有关自第一帧起经过了多少秒的信息。样例代码使用它来使影片的颗粒动起来,这osg_FrameTime
在每个帧中都会有所不同。
对于静态谷物,必须osg_FrameTime
大量更换薄膜。为避免看到模式,您可以尝试使用不同的数字。
要创建点或胶片颗粒斑点,请同时使用坐标,x和y。如果使用x,则仅显示垂直线,如果使用y,则仅显示水平线。在代码中,一个坐标与另一个坐标相乘以破坏对角线对称性。当然,您可以摆脱坐标乘数,并获得完全可以接受的降雨效果。请注意,对于雨水的动画效果,需要乘以输出sin
上osg_FrameTime
。尝试使用x和y坐标更改雨的方向。对于向下淋浴,仅保留x坐标。 input = (gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime) * toRadians frame(10000 * sin(input)) = fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) =
sin
用作哈希函数。片段坐标将使用输出值进行哈希处理sin
。因此,出现了一个方便的属性-无论输入数据(大还是小),输出间隔都将在负一到一的范围内。 fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = fract(6400.723818964882) = 0.723818964882
sin
结合fract
还用作伪随机数生成器。 >>> [floor(fract(4 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 0, 1, 2, 2, 3, 4, 4, 5, 6] >>> [floor(fract(10000 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 4, 8, 0, 2, 1, 7, 0, 0, 5]
首先查看第一行数字,然后查看第二行。每一行都是确定性的,但第二行中的模式不如第二行中的那么明显。因此,尽管事实是输出是fract(10000 * sin(...))
确定性的,但人们认为该模式要弱得多。在这里,我们看到因子sin
是如何先是1,然后是10,然后是100,然后是1000。随着输出值的乘数增加,sin
模式变得不太明显。因此,代码sin
乘以10,000。片段颜色
将片段的坐标转换为UV坐标。使用这些UV坐标,我们寻找当前片段的纹理颜色。
将值更改为随机亮度,然后将其添加到颜色中。
设置片段的颜色,仅此而已。源代码
致谢