初学者3D游戏着色器:效果

[ 第一部分 ]

在处理了基础知识之后,在本文的这一部分中,我们实现了诸如对象轮廓,光晕,SSAO,模糊,景深,像素化等效果。

概述



在场景的几何图形周围创建轮廓,使游戏具有类似于漫画或卡通的独特外观。

扩散材料


轮廓着色器需要输入纹理来识别和着色边缘。 此类传入纹理的候选对象可以是材质的漫反射颜色,漫反射纹理的颜色,顶点法线,甚至可以是法线贴图的颜色。

uniform struct { vec4 diffuse ; } p3d_Material; out vec4 fragColor; void main() { vec3 diffuseColor = p3d_Material.diffuse.rgb; fragColor = vec4(diffuseColor, 1); } 

这是一个小的片段着色器,可将几何材料的漫反射颜色渲染到帧缓冲区纹理中。 来自帧缓冲区的漫反射色纹理将成为路径着色器的输入纹理。


这是帧缓冲区中材质的漫反射颜色的纹理,该纹理显示了我们在Blender中设置的颜色。 轮廓着色器将识别场景中的边缘并为其着色。

应该注意的是,如果场景的某些部分没有它们自己的材质的漫反射色,则材质的漫反射色将不起作用。

创建边缘



创建边缘类似于在GIMP中使用边缘识别过滤器。

此着色技术的所有计算都在片段着色器中执行。 要为顶点着色器创建轮廓,只需将矩形网格的四个顶点传递到输出以适合屏幕即可。

 // ... uniform sampler2D materialDiffuseTexture; // ... vec2 texSize = textureSize(materialDiffuseTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy; // ... 

在开始识别边缘之前,您需要准备传入的纹理,我们将使用该纹理。 由于纹理具有屏幕大小,因此我们可以计算UV坐标,知道片段的坐标和传入纹理的大小。

  // ... int separation = 1; // ... 

可以根据您的口味定制separation 。 间隔越大,边缘或线条越粗。

  // ... float threshold = 0; // ... vec4 mx = vec4(0); vec4 mn = vec4(1); int x = -1; int y = -1; for (int i = 0; i < 9; ++i) { vec4 color = texture ( materialDiffuseTexture , (texCoord + (vec2(x, y) * separation)) / texSize ); mx = max(color, mx); mn = min(color, mn); x += 1; if (x >= 2) { x = -1; y += 1; } } float alpha = ((mx.r + mx.g + mx.b) / 3) - ((mn.r + mn.g + mn.b) / 3); if (alpha > threshold) { alpha = 1; } // ... 


边缘识别技术可发现传入纹理的颜色变化。 着眼于当前片段,它使用3x3片段窗口查找九个样本中最亮和最暗的颜色。 然后,她从一种颜色的亮度中减去另一种颜色的亮度,得到它们的差值。

  // ... vec3 lineRgb = vec3(0.012, 0.014, 0.022); // ... vec4 lineColor = vec4(lineRgb, alpha); // ... fragColor = lineColor; // ... 

此差异用于输出颜色的Alpha通道。 如果没有差异,则不会绘制边缘或线条。 如果存在差异,则绘制边缘。

  // ... float threshold = 0; // ... if (alpha > threshold) { alpha = 1; } // ... 

尝试试验阈值。 现在为零。 任何非零值都将成为边;可以更改此阈值。 这对于差异较小的噪声较大的传入纹理特别有用。 对于嘈杂的传入纹理,通常只需要为较大的差异创建轮廓。

源代码



雾气



雾(或薄雾,在Blender中被称为雾)在场景中增添了大气的阴霾,创造出神秘而柔软的突出部分。 当某些几何形状突然落入相机可见性金字塔时,会出现突出部分。

 // ... uniform struct p3d_FogParameters { vec4 color ; float start ; float end ; } p3d_Fog; // ... 

Panda3D具有方便的数据结构,其中包含所有雾参数,但是您可以将其手动传输到着色器。

  // ... float fogIntensity = clamp ( ( p3d_Fog.end - vertexPosition.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; // ... 

在该代码示例中,线性模型用于计算从相机移开时雾的亮度。 相反,您可以使用指数模型。 雾的亮度在雾开始之前或开始时为零。 当顶点位置接近雾的末端时,雾fogIntensity接近统一。 对于雾结束后的所有顶点,从上方将雾fogIntensity限制为1。

  // ... fragColor = mix ( outputColor , p3d_Fog.color , fogIntensity ); // ... 

根据雾的亮度,我们将雾的颜色与输出颜色混合。 随着fogIntensity趋于一致,将会有越来越少的outputColor和越来越多的雾色。 当fogIntensity达到单位时,将仅保留雾的颜色。

轮廓上的雾




 // ... uniform sampler2D positionTexture; // ... vec4 position = texture(positionTexture, texCoord / texSize); float fogIntensity = clamp ( ( p3d_Fog.end - position.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; vec4 lineWithFogColor = mix ( lineColor , p3d_Fog.color , fogIntensity ); fragColor = vec4(lineWithFogColor.rgb, alpha); // ... 

路径着色器将雾化到边缘的颜色,以获得更整体的图像。 如果他不这样做,轮廓的几何形状将被雾遮盖,这看起来很奇怪。 但是,他仍然使用铣刀在舞台几何图形的最外边缘上创建轮廓,因为这些边缘超出了几何图形-到达了没有顶点位置的位置。

positionTexture是一个帧缓冲区纹理,其中包含视图空间的顶点位置。 当我们实现SSAO着色器时,您将了解到这一点。

源代码



布卢姆



向场景中添加花开可以使照明模型产生令人信服的幻觉。 发光物体变得更有说服力,并且光反射会接收到更多的辐射。

  //... float separation = 3; int samples = 15; float threshold = 0.5; float amount = 1; // ... 

您可以根据自己的喜好自定义这些设置。 分离会增加模糊的大小。 样本确定模糊的强度。 阈值确定此影响将影响什么和将不会受到什么影响。 数量控制光晕输出的量。

  // ... int size = samples; int size2 = size * size; int x = 0; int y = 0; // ... float value = 0; vec4 result = vec4(0); vec4 color = vec4(0); // ... for (int i = 0; i < size2; ++i) { // ... } // ... 

该技术首先将窗口大小的samples传递到相对于当前片段居中的samples上。 它看起来像一个用于创建路径的窗口。

  // ... color = texture ( bloomTexture , ( gl_FragCoord.xy + vec2(x * separation, y * separation) ) / texSize ); value = ((0.3 * color.r) + (0.59 * color.g) + (0.11 * color.b)); if (value < threshold) { color = vec4(0); } result += color; // ... 

此代码从传入的纹理获取颜色,并将红色,绿色和蓝色的值转换为灰度值。 如果灰度值小于阈值,则它将丢弃此颜色,使其变为黑色。

通过窗口中的所有样本,它将所有result值累加。

  // ... result = result / size2; // ... 

完成样本收集后,他将颜色样本的总和除以所采样的数目。 结果是片段本身及其邻居的中间颜色。 通过对每个片段执行此操作,我们得到了模糊的图像。 这种模糊称为框模糊。


在这里,您将看到执行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>到纹理。

  // ... FrameBufferProperties fbp = FrameBufferProperties::get_default(); // ... fbp.set_rgba_bits(32, 32, 32, 32); fbp.set_rgb_color(true); fbp.set_float_color(true); // ... 

这是一个示例代码,该代码准备用于存储顶点位置的帧缓冲区纹理。 他需要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轴指向上方的顶点的位置。

样品


为了确定任何单个片段的环境光遮挡值,我们需要对周围区域进行采样。

  // ... for (int i = 0; i < numberOfSamples; ++i) { LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) ).normalized(); float rand = randomFloats(generator); sample[0] *= rand; sample[1] *= rand; sample[2] *= rand; float scale = (float) i / (float) numberOfSamples; scale = lerp(0.1, 1.0, scale * scale); sample[0] *= scale; sample[1] *= scale; sample[2] *= scale; ssaoSamples.push_back(sample); } // ... 

样本代码生成分布在半球中的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的间隔,以使其从负一变为一。

噪音


  // ... for (int i = 0; i < 16; ++i) { LVecBase3f noise = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , 0.0 ); ssaoNoise.push_back(noise); } // ... 

为了很好地覆盖采样区域,我们需要生成噪声向量。 这些噪声矢量可使样品围绕表面顶部旋转。

环境遮挡



SSAO通过采样片段周围的可视空间来完成其任务。 表面以下的样品越多,片段的颜色越深。 这些样本位于片段中,并指示顶点法线的大致方向。 每个样本用于在帧缓冲区位置的纹理中搜索一个位置。 将返回的位置与样本进行比较。 如果样品离摄像机的位置比该位置远,则样品被挡住。


在这里,您可以看到采样表面上方的空间以进行遮挡。

  // ... float radius = 1.1; float bias = 0.026; float lowerRange = -2; float upperRange = 2; // ... 

像其他一些技术一样,SSAO着色器具有几个控制参数,可以更改这些控制参数以获得所需的外观。 偏差会加到样品到相机的距离上。 此参数可用于消除污渍。 半径增加或减少样本空间的覆盖区域。 lowerRange和upperRange将因子度量标准范围从[0, 1]更改为您选择的任何值。 通过增加范围,可以增加对比度。

  // ... vec4 position = texture(positionTexture, texCoord); vec3 normal = texture(normalTexture, texCoord).xyz; int noiseX = int(gl_FragCoord.x - 0.5) % 4; int noiseY = int(gl_FragCoord.y - 0.5) % 4; vec3 random = noise[noiseX + (noiseY * 4)]; // ... 

我们获得位置,法线和随机向量以备将来使用。 回想一下,在代码示例中,创建了16个随机向量。 根据当前片段的屏幕位置选择一个随机向量。

  // ... vec3 tangent = normalize(random - normal * dot(random, normal)); vec3 binormal = cross(normal, tangent); mat3 tbn = mat3(tangent, binormal, normal); // ... 

使用随机向量和法线向量,我们收集切线,双法线和法线的矩阵。 我们需要这个矩阵将样本向量从切线空间转换到测量空间。

  // ... float occlusion = NUM_SAMPLES; for (int i = 0; i < NUM_SAMPLES; ++i) { // ... } // ... 

有了一个矩阵,着色器可以循环遍历循环中的所有样本,减去未打开的样本数。

  // ... vec3 sample = tbn * samples[i]; sample = position.xyz + sample * radius; // ... 

使用矩阵,将样品放置在顶点/片段位置旁边,并按半径缩放比例。

  // ... vec4 offset = vec4(sample, 1.0); offset = lensProjection * offset; offset.xyz /= offset.w; offset.xyz = offset.xyz * 0.5 + 0.5; // ... 

使用样本在观察空间中的位置,我们将其从观察空间转换为裁剪空间,然后转换为UV空间。

 -1 * 0.5 + 0.5 = 0 1 * 0.5 + 0.5 = 1 

不要忘记剪切空间的分量在负一到一的范围内,UV坐标在零到一的范围内。 要将剪贴空间的坐标转换为UV坐标,请将它们乘以一秒钟,然后加一秒。

  // ... vec4 offsetPosition = texture(positionTexture, offset.xy); float occluded = 0; if (sample.y + bias <= offsetPosition.y) { occluded = 0; } else { occluded = 1; } // ... 

使用通过将3D样本投影到2D位置纹理上获得的UV偏移坐标,我们找到相应的位置矢量。 这使我们从观察空间到裁剪空间再到UV空间,然后又回到观察空间。 着色器运行此循环以确定样品后面,样品位置或样品前面是否存在任何几何形状。 如果样本位于几何图形的前面或某个几何图形中,则相对于重叠片段,根本就不会考虑该样本。 如果样本在某些几何形状后面,则相对于重叠片段考虑该样本。

  // ... float intensity = smoothstep ( 0.0 , 1.0 , radius / abs(position.y - offsetPosition.y) ); occluded *= intensity; occlusion -= occluded; // ... 

现在,根据其在半径内或半径外的距离向此采样位置添加权重。 然后,从阻塞度量中减去此样本,因为它假定所有样本在循环之前都已重叠。

  // ... occlusion /= NUM_SAMPLES; // ... fragColor = vec4(vec3(occlusion), position.a); // ... 

将重叠数除以样本数,将遮挡指示器从间隔[0, NUM_SAMPLES]转换为间隔[0, 1] 。 零表示完全遮挡,单位表示没有遮挡。 现在,将遮挡指标分配给片段颜色,仅此而已。

请注意,在示例代码中,为Alpha通道分配了来自帧缓冲区的位置纹理的Alpha值,以避免背景重叠。

模糊



SSAO帧缓冲区的纹理有点嘈杂,因此应对其进行模糊处理以使其平滑。

  // ... for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( ssaoTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ).rgb; xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

SSAO模糊着色器是常规的框模糊。 像Bloom着色器一样,它在传入的纹理上绘制一个窗口,并将每个片段及其相邻元素的值平均。

请注意, parameters.x是分隔参数。

环境颜色


  // ... vec2 ssaoBlurTexSize = textureSize(ssaoBlurTexture, 0).xy; vec2 ssaoBlurTexCoord = gl_FragCoord.xy / ssaoBlurTexSize; float ssao = texture(ssaoBlurTexture, ssaoBlurTexCoord).r; vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex * ssao; // ... 

SSAO面临的最后挑战还是照明计算。 在这里,我们了解了如何在纹理SSAO纹理缓冲区中找到遮挡并将其包含在环境光的计算中。

源代码



景深



景深也是这样一种影响,如果了解了哪些,没有它就无法生存。 从艺术的角度来看,您可以使用它来吸引观看者对特定对象的关注。 但是在一般情况下,以一点点努力为代价的景深会增加很大一部分真实感。

聚焦


第一步是使场景完全聚焦。 渲染到帧缓冲区纹理。 这将是景深缓冲区的输入值之一。

失去重点


  // ... vec4 result = vec4(0); for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( blurTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ); xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

第二步是使场景模糊,好像它完全不在焦点上一样。 与Bloom和SSAO一样,您可以使用盒子模糊。 将此散焦场景渲染为帧缓冲区纹理。 这将是景深着色器的另一个输入值。

请注意, parameters.x是分隔参数。

混乱




  // ... float focalLengthSharpness = 100; float blurRate = 6; // ... 

您可以根据自己的喜好自定义这些选项。 focalLengthSharpness影响场景在焦距下的散焦效果。 focalLengthSharpness越小,场景在焦距处的散焦就越多。 当远离焦距时, blurRate影响场景模糊的速度。 blurRate ,远离焦点时场景的模糊越少。

  // ... vec4 focusColor = texture(focusTexture, texCoord); vec4 outOfFocusColor = texture(outOfFocusTexture, texCoord); // ... 

我们将需要焦点和散焦图像中的颜色。

  // ... vec4 position = texture(positionTexture, texCoord); // ... 

我们可能还需要在查看空间中顶点的位置。 您可以从用于SSAO的帧缓冲区中重新应用位置的纹理。

  // ... float blur = clamp ( pow ( blurRate , abs(position.y - focalLength.x) ) / focalLengthSharpness , 0 , 1 ); // ... fragColor = mix(focusColor, outOfFocusColor, blur); // ... 

在这里发生了混乱。越接近blur1,它将使用的越多outOfFocusColor零值blur表示此片段完全在焦点上。有了blur >= 1这个片段就完全散焦了。

源代码



后发化



后幅化或颜色采样是减少图像中唯一颜色数量的过程。您可以使用此着色器使游戏具有漫画或复古外观。如果将其与轮廓线结合使用,则会获得真正的卡通风格。

  // ... float levels = 8; // ... 

您可以尝试使用此参数。它越大,结果将保留的花越多。

  // ... vec4 texColor = texture(posterizeTexture, texCoord); // ... 

我们将需要传入的颜色。

  // ... vec3 grey = vec3((texColor.r + texColor.g + texColor.b) / 3.0); vec3 grey1 = grey; grey = floor(grey * levels) / levels; texColor.rgb += (grey - grey1); // ... 

我还没有看到这样的后代化方法。对其进行检查后,我发现与传统方法相比,它可以产生更漂亮的结果。要减少调色板,请先将颜色转换为灰度值。我们通过将颜色绑定到某一级别来离散化颜色。我们计算灰度的离散值与灰度的非离散值之间的差。将此差异添加到输入颜色。这种差异是颜色必须增加/减少以达到灰度离散值的数量。

  // ... fragColor = texColor; // ... 

不要忘记将输入颜色的值分配给片段的颜色。

Cel底纹



后处理可以使图片看起来像cel阴影,因为cel阴影是将漫反射和漫反射颜色离散为离散阴影的过程。我们只想使用纯色漫反射颜色,而没有法线贴图的精细细节和较小的值levels

源代码



像素化



3D游戏的像素化可以赋予它有趣的外观,或者可以节省您手动创建所有像素图所需的时间。将其与后代化相结合,可以创建出真正的复古外观。

  // ... int pixelSize = 5; // ... 

您可以自己调整像素大小。它越大,图像越粗糙。



  // ... float x = int(gl_FragCoord.x) % pixelSize; float y = int(gl_FragCoord.y) % pixelSize; x = floor(pixelSize / 2.0) - x; y = floor(pixelSize / 2.0) - y; x = gl_FragCoord.x + x; y = gl_FragCoord.y + y; // ... 

此技术将每个片段附加到其最近的不重叠像素大小的窗口的中心。这些窗口在传入纹理的顶部对齐。窗口中心的片段确定了其窗口中其他片段的颜色。

  // ... fragColor = texture(pixelizeTexture, vec2(x, y) / texSize); // ... 

确定所需片段的坐标后,从传入纹理中获取其颜色并将其分配给片段颜色。

源代码



锐化



锐化效果(锐化)可提高图像边缘的对比度。当图形变得太软时,它会派上用场。

  // ... float amount = 0.8; // ... 

通过更改值,我们可以控制结果的清晰度。如果值为零,则图像不会改变。值为负值时,图像开始看起来很奇怪。

  // ... float neighbor = amount * -1; float center = amount * 4 + 1; // ... 

相邻的片段乘以amount * -1当前片段乘以amount * 4 + 1

  // ... vec3 color = texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 1) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x - 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 0) / texSize).rgb * center + texture(sharpenTexture, vec2(gl_FragCoord.x + 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y - 1) / texSize).rgb * neighbor ; // ... 

相邻的片段位于顶部,底部,左侧和右侧。将邻居和当前片段乘以它们的值后,将结果相加。

  // ... fragColor = vec4(color, texture(sharpenTexture, texCoord).a); // ... 

此数量是片段的最终颜色。

源代码



胶片颗粒



薄膜的颗粒感(小剂量,而不是示例中的情况)可以增加真实感,直到消除此效果之前,该效果才可见。通常,这些缺陷使数字生成的图像更具说服力。

请注意,胶片颗粒通常是在显示之前应用于帧的最后一个效果。

价值


  // ... float amount = 0.1; // ... 

amount控制胶片颗粒的可见性。值越高,图片中的“雪”越多。

随机亮度


 // ... uniform float osg_FrameTime; //... float toRadians = 3.14 / 180; //... float randomIntensity = fract ( 10000 * sin ( ( gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime ) * toRadians ) ); // ... 

这段代码计算调整该值所需的随机亮度。

 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在每个帧中都会有所不同。

  // ... ( gl_FragCoord.x + gl_FragCoord.y * 8009 // Large number here. // ... 

对于静态谷物,必须osg_FrameTime大量更换薄膜为避免看到模式,您可以尝试使用不同的数字。



  // ... * sin ( ( gl_FragCoord.x + gl_FragCoord.y * someNumber // ... 

要创建点或胶片颗粒斑点,请同时使用坐标,x和y。如果使用x,则仅显示垂直线,如果使用y,则仅显示水平线。

在代码中,一个坐标与另一个坐标相乘以破坏对角线对称性。


当然,您可以摆脱坐标乘数,并获得完全可以接受的降雨效果。

请注意,对于雨水的动画效果,需要乘以输出sinosg_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。

片段颜色


  // ... vec2 texSize = textureSize(filmGrainTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 color = texture(filmGrainTexture, texCoord); // ... 

将片段的坐标转换为UV坐标。使用这些UV坐标,我们寻找当前片段的纹理颜色。

  // ... amount *= randomIntensity; color.rgb += amount; // ... 

将值更改为随机亮度,然后将其添加到颜色中。

  // ... fragColor = color; // ... 

设置片段的颜色,仅此而已。

源代码



致谢


Source: https://habr.com/ru/post/zh-CN453692/


All Articles