17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 01:39:20 +02:00

feat(render): ambient occlusion with bent normals darkens only the light of the surroundings instead of the whole image, so the sun and headlights keep their full strength; the occlusion and the bent normal of the previous frame are reprojected into the lighting of the scene, with the motion of the camera and of every vehicle and cab taken into account and with depth checks against disocclusion, the ambient light follows how much of the sky the bent normal sees, and reflections of the environment are occluded where the cone of visible directions around the bent normal cuts off the reflected lobe, only where the occlusion is clear enough that open flat panels and windows keep their grazing reflections

This commit is contained in:
maj00r
2026-10-14 18:02:35 +02:00
parent a24f9fae3f
commit 806dde70bf
12 changed files with 194 additions and 53 deletions

View File

@@ -6,6 +6,7 @@ struct _global
bool gfx_postfx_motionblur_enabled;
bool gfx_skippipeline;
bool gfx_extraeffects;
bool gfx_postfx_gtao_enabled;
bool gfx_shadergamma;
bool gfx_usegles;
};

View File

@@ -45,6 +45,7 @@ bool check_shader_allopts(std::string filename)
Global.gfx_postfx_motionblur_enabled = true;
Global.gfx_skippipeline = false;
Global.gfx_extraeffects = true;
Global.gfx_postfx_gtao_enabled = true;
Global.gfx_shadergamma = true;
if (!check_shader(filename))
suc = false;
@@ -55,6 +56,7 @@ bool check_shader_allopts(std::string filename)
Global.gfx_postfx_motionblur_enabled = false;
Global.gfx_skippipeline = true;
Global.gfx_extraeffects = false;
Global.gfx_postfx_gtao_enabled = false;
Global.gfx_shadergamma = false;
if (!check_shader(filename))
suc = false;

View File

@@ -11,6 +11,7 @@ void gl::glsl_common_setup()
"#define MOTIONBLUR_ENABLED " + std::to_string((int)Global.gfx_postfx_motionblur_enabled) + "\n" +
"#define POSTFX_ENABLED " + std::to_string((int)!Global.gfx_skippipeline) + "\n" +
"#define EXTRAEFFECTS_ENABLED " + std::to_string((int)Global.gfx_extraeffects) + "\n" +
"#define GTAO_ENABLED " + std::to_string((int)(Global.gfx_postfx_gtao_enabled && !Global.gfx_skippipeline)) + "\n" +
"#define MAX_LIGHTS " + std::to_string(MAX_LIGHTS) + "U\n" +
"#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" +
"#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" +
@@ -77,6 +78,8 @@ void gl::glsl_common_setup()
vec4 wiper_pos;
vec4 wiper_timer_out;
vec4 wiper_timer_return;
mat4 gtao_reprojection;
vec4 gtao_params;
};
// Per-instance modelview matrices for GPU-instanced draws.

View File

@@ -347,6 +347,7 @@ void gl::program::init()
glUniform1i(glGetUniformLocation(*this, "shadowmap"), gl::SHADOW_TEX);
glUniform1i(glGetUniformLocation(*this, "envmap"), gl::ENV_TEX);
glUniform1i(glGetUniformLocation(*this, "headlightmap"), gl::HEADLIGHT_TEX);
glUniform1i(glGetUniformLocation(*this, "gtaomap"), gl::GTAO_TEX);
GLuint index;

View File

@@ -46,6 +46,7 @@ namespace gl
const size_t SHADOW_TEX = MAX_TEXTURES + 0;
const size_t ENV_TEX = MAX_TEXTURES + 1;
const size_t HEADLIGHT_TEX = MAX_TEXTURES + 2;
const size_t GTAO_TEX = MAX_TEXTURES + 3;
struct scene_ubs
{
@@ -58,9 +59,11 @@ namespace gl
glm::vec4 wiper_pos;
glm::vec4 wiper_timer_out;
glm::vec4 wiper_timer_return;
glm::mat4 gtao_reprojection { 1.f };
glm::vec4 gtao_params { 0.f };
};
static_assert(sizeof(scene_ubs) == 400, "bad size of ubs");
static_assert(sizeof(scene_ubs) == 480, "bad size of ubs");
const size_t MAX_PARAMS = 3;

View File

@@ -218,7 +218,6 @@ bool opengl33_renderer::Init(GLFWwindow *Window)
{
m_pfx_gtao = std::make_unique<gl::postfx>("gtao");
m_pfx_gtao_blur = std::make_unique<gl::postfx>("gtao_blur");
m_pfx_gtao_apply = std::make_unique<gl::postfx>("gtao_apply");
}
// Generate hemisphere kernel (z > 0 = toward surface normal)
@@ -579,16 +578,17 @@ bool opengl33_renderer::init_viewport(viewport_config &vp)
int gtao_h = std::max(1, vp.height / 2);
vp.gtao_tex = std::make_unique<opengl_texture>();
vp.gtao_tex->alloc_rendertarget(GL_R8, GL_RED, gtao_w, gtao_h, 1, 1, GL_CLAMP_TO_EDGE);
vp.gtao_tex->alloc_rendertarget(GL_RGBA16F, GL_RGBA, gtao_w, gtao_h, 1, 1, GL_CLAMP_TO_EDGE);
vp.gtao_fb = std::make_unique<gl::framebuffer>();
vp.gtao_fb->attach(*vp.gtao_tex, GL_COLOR_ATTACHMENT0);
if (!vp.gtao_fb->is_complete()) { ErrorLog("gtao framebuffer setup failed"); return false; }
vp.gtao_blur_tex = std::make_unique<opengl_texture>();
vp.gtao_blur_tex->alloc_rendertarget(GL_R8, GL_RED, gtao_w, gtao_h, 1, 1, GL_CLAMP_TO_EDGE);
vp.gtao_blur_tex->alloc_rendertarget(GL_RGBA16F, GL_RGBA, gtao_w, gtao_h, 1, 1, GL_CLAMP_TO_EDGE);
vp.gtao_blur_fb = std::make_unique<gl::framebuffer>();
vp.gtao_blur_fb->attach(*vp.gtao_blur_tex, GL_COLOR_ATTACHMENT0);
if (!vp.gtao_blur_fb->is_complete()) { ErrorLog("gtao blur framebuffer setup failed"); return false; }
vp.gtao_history = false;
}
if (!vp.main2_fb->is_complete())
@@ -977,6 +977,17 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode)
scene_ubo->update(scene_ubs);
Render(&simulation::Environment);
if (Global.gfx_postfx_gtao_enabled && !Global.gfx_skippipeline && vp.gtao_blur_tex)
{
m_gtao_vp = &vp;
m_gtao_projection = scene_ubs.projection;
m_gtao_view = glm::dmat4{ glm::dmat3{ glm::mat3{ m_renderpass.pass_camera.modelview() } } } * glm::translate(glm::dmat4{ 1.0 }, -m_renderpass.pass_camera.position());
m_gtao_models.clear();
vp.gtao_blur_tex->bind(gl::GTAO_TEX);
scene_ubs.gtao_params = glm::vec4(vp.gtao_history ? 1.f : 0.f, Global.gfx_postfx_gtao_intensity, 0.f, 0.f);
setup_gtao_motion(nullptr);
}
if( Global.gfx_shadowmap_enabled )
setup_shadow_bind_map();
@@ -1049,6 +1060,13 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode)
Timer::subsystem.gfx_color.stop();
if (m_gtao_vp != nullptr)
{
scene_ubs.gtao_params.x = 0.f;
scene_ubo->update(scene_ubs);
opengl_texture::unbind(gl::GTAO_TEX);
}
// store draw stats
m_colorpass.draw_stats = m_renderpass.draw_stats;
@@ -1094,17 +1112,20 @@ if (!Global.gfx_skippipeline)
model_ubo->update(model_ubs);
glViewport(0, 0, gtao_w, gtao_h);
m_pfx_gtao->apply({ vp.main_texd.get() }, vp.gtao_fb.get());
m_pfx_gtao_blur->apply({ vp.gtao_tex.get(), vp.main_texd.get() }, vp.gtao_blur_fb.get());
m_pfx_gtao_blur->apply({ vp.gtao_tex.get() }, vp.gtao_blur_fb.get());
glViewport(0, 0, vp.width, vp.height);
vp.main_fb->setup_drawing(1);
glEnable(GL_BLEND);
glBlendFunc(GL_ZERO, GL_SRC_COLOR);
m_pfx_gtao_apply->apply({ vp.gtao_blur_tex.get() }, vp.main_fb.get(), false);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
if (!blend)
glDisable(GL_BLEND);
if (blend)
glEnable(GL_BLEND);
if (m_gtao_vp == &vp)
{
vp.gtao_projection = m_gtao_projection;
vp.gtao_view = m_gtao_view;
vp.gtao_models = std::move(m_gtao_models);
vp.gtao_history = true;
}
}
m_gtao_vp = nullptr;
if (Global.gfx_postfx_motionblur_enabled)
{
@@ -1406,6 +1427,7 @@ bool opengl33_renderer::Render_lowpoly( TDynamicObject *Dynamic, float const Squ
::glPushMatrix();
::glTranslated( originoffset.x, originoffset.y, originoffset.z );
::glMultMatrixd( glm::value_ptr(Dynamic->mMatrix) );
setup_gtao_motion( Dynamic );
}
// HACK: reduce light level for vehicle interior if there's strong global lighting source
if( false == Alpha ) {
@@ -1454,6 +1476,7 @@ bool opengl33_renderer::Render_lowpoly( TDynamicObject *Dynamic, float const Squ
}
::glPopMatrix();
setup_gtao_motion( nullptr );
// TODO: check if this reset is needed. In theory each object should render all parts based on its own instance data anyway?
if( Dynamic->btnOn ) {
@@ -1489,6 +1512,24 @@ bool opengl33_renderer::Render_coupler_adapter( TDynamicObject *Dynamic, float c
}
// creates dynamic environment cubemap
void opengl33_renderer::setup_gtao_motion(TDynamicObject const *Dynamic)
{
if (m_gtao_vp == nullptr || m_renderpass.draw_mode != rendermode::color)
return;
glm::dmat4 motion{ 1.0 };
if (Dynamic != nullptr)
{
glm::dmat4 const model{ glm::translate(glm::dmat4{ 1.0 }, Dynamic->GetPosition()) * Dynamic->mMatrix };
m_gtao_models[Dynamic] = model;
auto const previous{ m_gtao_vp->gtao_models.find(Dynamic) };
if (previous != m_gtao_vp->gtao_models.end())
motion = previous->second * glm::inverse(model);
}
scene_ubs.gtao_reprojection = m_gtao_vp->gtao_projection * glm::mat4{ m_gtao_vp->gtao_view * motion * glm::inverse(m_gtao_view) };
scene_ubo->update(reinterpret_cast<uint8_t const *>(&scene_ubs.gtao_reprojection), static_cast<int>(offsetof(gl::scene_ubs, gtao_reprojection)), sizeof(glm::mat4) + sizeof(glm::vec4));
}
bool opengl33_renderer::Render_reflections(viewport_config &vp)
{
if( Global.reflectiontune.update_interval == 0 ) { return false; }
@@ -3859,6 +3900,7 @@ bool opengl33_renderer::Render(TDynamicObject *Dynamic)
::glPushMatrix();
::glTranslated(originoffset.x, originoffset.y, originoffset.z);
::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix));
setup_gtao_motion(Dynamic);
switch (m_renderpass.draw_mode)
{
@@ -3944,6 +3986,7 @@ bool opengl33_renderer::Render(TDynamicObject *Dynamic)
}
::glPopMatrix();
setup_gtao_motion(nullptr);
model_ubs.future = future_stack;
@@ -3983,6 +4026,7 @@ bool opengl33_renderer::Render_cab(TDynamicObject const *Dynamic, float const Li
auto const originoffset = Dynamic->GetPosition() - m_renderpass.pass_camera.position();
::glTranslated(originoffset.x, originoffset.y, originoffset.z);
::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix));
setup_gtao_motion(Dynamic);
switch (m_renderpass.draw_mode)
{
@@ -4046,6 +4090,7 @@ bool opengl33_renderer::Render_cab(TDynamicObject const *Dynamic, float const Li
}
// post-render restore
::glPopMatrix();
setup_gtao_motion(nullptr);
}
return true;
@@ -5008,6 +5053,7 @@ bool opengl33_renderer::Render_Alpha(TDynamicObject *Dynamic)
::glTranslated(originoffset.x, originoffset.y, originoffset.z);
::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix));
setup_gtao_motion(Dynamic);
if (Dynamic->fShade > 0.0f)
{
@@ -5044,6 +5090,7 @@ bool opengl33_renderer::Render_Alpha(TDynamicObject *Dynamic)
}
::glPopMatrix();
setup_gtao_motion(nullptr);
model_ubs.future = future_stack;

View File

@@ -255,6 +255,10 @@ class opengl33_renderer : public gfx_renderer {
std::unique_ptr<opengl_texture> gtao_blur_tex;
std::unique_ptr<gl::framebuffer> gtao_fb;
std::unique_ptr<gl::framebuffer> gtao_blur_fb;
bool gtao_history = false;
glm::mat4 gtao_projection;
glm::dmat4 gtao_view;
std::unordered_map<void const *, glm::dmat4> gtao_models;
// LDR backbuffer for offscreen rendering
std::unique_ptr<gl::framebuffer> backbuffer_fb;
@@ -283,6 +287,7 @@ class opengl33_renderer : public gfx_renderer {
void Render_pass(viewport_config &vp, rendermode const Mode);
// creates dynamic environment cubemap
bool Render_reflections(viewport_config &vp);
void setup_gtao_motion(TDynamicObject const *Dynamic);
bool Render(world_environment *Environment);
void Render(scene::basic_region *Region);
void Render(section_sequence::iterator First, section_sequence::iterator Last);
@@ -520,7 +525,10 @@ class opengl33_renderer : public gfx_renderer {
std::unique_ptr<gl::postfx> m_pfx_gtao;
std::unique_ptr<gl::postfx> m_pfx_gtao_blur;
std::unique_ptr<gl::postfx> m_pfx_gtao_apply;
viewport_config *m_gtao_vp = nullptr;
glm::mat4 m_gtao_projection;
glm::dmat4 m_gtao_view;
std::unordered_map<void const *, glm::dmat4> m_gtao_models;
std::unique_ptr<gl::program> m_shadow_shader;
std::unique_ptr<gl::program> m_alpha_shadow_shader;

17
shaders/gtao_common.glsl Normal file
View File

@@ -0,0 +1,17 @@
vec2 octahedral_encode(vec3 n)
{
n /= abs(n.x) + abs(n.y) + abs(n.z);
vec2 o = n.xy;
if (n.z < 0.0)
o = (1.0 - abs(n.yx)) * vec2(n.x >= 0.0 ? 1.0 : -1.0, n.y >= 0.0 ? 1.0 : -1.0);
return o;
}
vec3 octahedral_decode(vec2 o)
{
vec3 n = vec3(o, 1.0 - abs(o.x) - abs(o.y));
float t = clamp(-n.z, 0.0, 1.0);
n.x += (n.x >= 0.0 ? -t : t);
n.y += (n.y >= 0.0 ? -t : t);
return normalize(n);
}

View File

@@ -4,6 +4,68 @@ uniform sampler2DArrayShadow shadowmap;
#endif
uniform sampler2D headlightmap;
#if GTAO_ENABLED
uniform sampler2D gtaomap;
#include <gtao_common.glsl>
vec4 gtao_sample()
{
vec4 none = vec4(1.0, 0.0, 0.0, 0.0);
if (gtao_params.x < 0.5)
return none;
vec4 clip = gtao_reprojection * vec4(f_pos.xyz, 1.0);
if (clip.w <= 0.0)
return none;
vec2 uv = clip.xy / clip.w * 0.5 + 0.5;
if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0))))
return none;
ivec2 size = textureSize(gtaomap, 0);
vec2 position = uv * vec2(size) - 0.5;
ivec2 base = ivec2(floor(position));
vec2 f = position - vec2(base);
float visibility = 0.0;
vec3 bent = vec3(0.0);
float weights = 0.0;
for (int i = 0; i < 4; ++i)
{
ivec2 offset = ivec2(i & 1, i >> 1);
vec4 tap = texelFetch(gtaomap, clamp(base + offset, ivec2(0), size - 1), 0);
float difference = abs(tap.g - clip.w) / (clip.w * 0.08 + 0.05);
float weight = (offset.x == 1 ? f.x : 1.0 - f.x) * (offset.y == 1 ? f.y : 1.0 - f.y) * max(1.0 - difference, 0.0);
visibility += tap.r * weight;
bent += octahedral_decode(tap.ba) * weight;
weights += weight;
}
if (weights < 1e-3 || dot(bent, bent) < 1e-8)
return none;
return vec4(clamp(visibility / weights, 0.0, 1.0), normalize(bent));
}
float gtao_cone_overlap(float cos1, float cos2, float cosd)
{
float r1 = acos(cos1);
float r2 = acos(cos2);
float d = acos(clamp(cosd, -1.0, 1.0));
float smaller = 1.0 - max(cos1, cos2);
if (min(r1, r2) <= max(r1, r2) - d)
return smaller;
if (r1 + r2 <= d)
return 0.0;
float delta = abs(r1 - r2);
return smaller * smoothstep(0.0, 1.0, 1.0 - clamp((d - delta) / max(r1 + r2 - delta, 1e-4), 0.0, 1.0));
}
float gtao_specular(vec4 gtao, vec3 normal, vec3 reflected, float roughness)
{
vec3 bent = (dot(gtao.yzw, gtao.yzw) > 0.5 ? gtao.yzw : normal);
float coslobe = clamp(exp2(-3.32193 * roughness * roughness), 0.0, 0.999);
float occluded = gtao_cone_overlap(sqrt(clamp(1.0 - gtao.x, 0.0, 1.0)), coslobe, dot(bent, reflected));
float open = gtao_cone_overlap(0.0, coslobe, dot(normal, reflected));
return mix(1.0, clamp(occluded / max(open, 1e-5), 0.0, 1.0), smoothstep(0.9, 0.5, gtao.x));
}
#endif
#include <envmapping.glsl>
#include <conversion.glsl>
@@ -298,6 +360,13 @@ vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float refl
fragcolor *= 1.0 - parallax_occlusion;
#endif
#if GTAO_ENABLED
vec4 gtao = gtao_sample();
vec3 gtao_normal = mat3(inv_view) * fragnormal;
vec3 gtao_bent = (dot(gtao.yzw, gtao.yzw) > 0.5 ? gtao.yzw : gtao_normal);
fragcolor *= pow(gtao.x, gtao_params.y) * min((0.75 + 0.25 * gtao_bent.y) / (0.75 + 0.25 * gtao_normal.y), 1.0);
#endif
vec3 emissioncolor = vec3(emission);
vec3 view_dir = normalize(-f_pos.xyz);
@@ -314,6 +383,9 @@ vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float refl
// mirroring the sky. `env_spec` is the colour to multiply the cubemap by.
vec3 env_spec = EnvBRDFApprox(F0, env_roughness, NdotV);
float env_spec_w = max(env_spec.r, max(env_spec.g, env_spec.b));
#if GTAO_ENABLED
env_spec *= gtao_specular(gtao, gtao_normal, normalize(mat3(inv_view) * reflect(f_pos.xyz, fragnormal)), env_roughness);
#endif
// Tint texture toward fully-saturated under strong env, weighted by the
// env BRDF (so a rough/matte surface no longer gets washed toward env hue)

View File

@@ -7,6 +7,8 @@ uniform sampler2D depth_tex;
#include <common>
#include <gtao_common.glsl>
const float PI = 3.14159265;
const float HALF_PI = 1.57079633;
@@ -28,7 +30,7 @@ void main()
vec2 size = vec2(textureSize(depth_tex, 0));
ivec2 center = ivec2(f_coords * size);
if (texelFetch(depth_tex, center, 0).r <= 0.0) {
out_color = vec4(1.0);
out_color = vec4(1.0, 60000.0, 0.0, 0.0);
return;
}
@@ -45,7 +47,7 @@ void main()
float radius = param[0].x;
float pixels = radius * projection[1][1] * 0.5 * size.y / -P.z;
if (pixels < 2.0) {
out_color = vec4(1.0);
out_color = vec4(1.0, -P.z, octahedral_encode(mat3(inv_view) * N));
return;
}
pixels = min(pixels, size.y * 0.25);
@@ -59,6 +61,7 @@ void main()
float jitter = fract(noise * 1.61803399);
float visibility = 0.0;
vec3 bent = vec3(0.0);
for (int d = 0; d < directions; ++d)
{
float phi = (float(d) + noise) * PI / float(directions);
@@ -95,8 +98,12 @@ void main()
float arc0 = (cosn + 2.0 * h0 * sin(n) - cos(2.0 * h0 - n)) * 0.25;
float arc1 = (cosn + 2.0 * h1 * sin(n) - cos(2.0 * h1 - n)) * 0.25;
visibility += projectedlength * (arc0 + arc1);
float middle = 0.5 * (h0 + h1);
bent += (V * cos(middle) + normalize(ortho) * sin(middle)) * projectedlength * (arc0 + arc1);
}
visibility = clamp(visibility / float(directions), 0.0, 1.0);
float fade = smoothstep(param[1].y * 0.75, param[1].y, -P.z);
out_color = vec4(mix(visibility, 1.0, fade));
bent = (dot(bent, bent) > 1e-8 ? normalize(bent) : N);
bent = normalize(mix(bent, N, fade));
out_color = vec4(mix(visibility, 1.0, fade), -P.z, octahedral_encode(mat3(inv_view) * bent));
}

View File

@@ -1,13 +0,0 @@
in vec2 f_coords;
layout(location = 0) out vec4 out_color;
#texture (ao_tex, 0, R)
uniform sampler2D ao_tex;
#include <common>
void main()
{
out_color = vec4(vec3(pow(clamp(texture(ao_tex, f_coords).r, 0.0, 1.0), param[0].y)), 1.0);
}

View File

@@ -2,44 +2,37 @@ in vec2 f_coords;
layout(location = 0) out vec4 out_color;
#texture (ao_tex, 0, R)
#texture (ao_tex, 0, RGBA)
uniform sampler2D ao_tex;
#texture (depth_tex, 1, R)
uniform sampler2D depth_tex;
#include <common>
float view_distance(vec2 uv)
{
ivec2 size = textureSize(depth_tex, 0);
float depth = texelFetch(depth_tex, clamp(ivec2(uv * vec2(size)), ivec2(0), size - 1), 0).r;
if (depth <= 0.0)
return 1e6;
float ndcz = (param[1].x > 0.5 ? depth : depth * 2.0 - 1.0);
return projection[3][2] / (ndcz + projection[2][2]);
}
#include <gtao_common.glsl>
void main()
{
vec2 texel = 1.0 / vec2(textureSize(ao_tex, 0));
float center = view_distance(f_coords);
if (center >= 1e6) {
out_color = vec4(1.0);
ivec2 size = textureSize(ao_tex, 0);
ivec2 center = ivec2(f_coords * vec2(size));
vec4 centersample = texelFetch(ao_tex, center, 0);
float centerdistance = centersample.g;
if (centerdistance >= 60000.0) {
out_color = centersample;
return;
}
float sum = 0.0;
float visibility = 0.0;
vec3 bent = vec3(0.0);
float weights = 0.0;
for (int y = -2; y <= 2; ++y)
{
for (int x = -2; x <= 2; ++x)
{
vec2 uv = f_coords + vec2(float(x), float(y)) * texel;
float difference = (view_distance(uv) - center) / (center * 0.02 + 0.01);
vec4 tap = texelFetch(ao_tex, clamp(center + ivec2(x, y), ivec2(0), size - 1), 0);
float difference = (tap.g - centerdistance) / (centerdistance * 0.02 + 0.01);
float weight = exp(-float(x * x + y * y) / 4.5 - difference * difference);
sum += texture(ao_tex, uv).r * weight;
visibility += tap.r * weight;
bent += octahedral_decode(tap.ba) * weight;
weights += weight;
}
}
out_color = vec4(sum / max(weights, 1e-4));
bent = (dot(bent, bent) > 1e-8 ? normalize(bent) : octahedral_decode(centersample.ba));
out_color = vec4(visibility / max(weights, 1e-4), centerdistance, octahedral_encode(bent));
}