xc3_wgpu 0.22.0

Xenoblade Chronicles model rendering library
Documentation
// PerPass resources.
struct DebugSettings {
    render_mode: u32,
    channel: i32
}

@group(0) @binding(0)
var<uniform> debug_settings: DebugSettings;

struct RenderSettings {
    mat_id: u32
}

// PerPass resources that may change when resized.
// TODO: Where to put this since it's only used for the toon shader?
@group(0) @binding(1)
var g_toon_grad: texture_2d<f32>;

@group(0) @binding(2)
var shared_sampler: sampler;

// "gTCol" in "clustered" in monolib/shader/shd_lgt.wishp.
@group(1) @binding(0)
var g_color: texture_2d<f32>;

// "gTEtc" in "clustered" in monolib/shader/shd_lgt.wishp.
@group(1) @binding(1)
var g_etc_buffer: texture_2d<f32>;

// "gTNom" in "clustered" in monolib/shader/shd_lgt.wishp.
@group(1) @binding(2)
var g_normal: texture_2d<f32>;

@group(1) @binding(3)
var g_velocity: texture_2d<f32>;

// "gTDep" in "clustered" in monolib/shader/shd_lgt.wishp.
@group(1) @binding(4)
var g_depth: texture_2d<f32>;

// TODO: the output at index 5 can be specular color or emission?
@group(1) @binding(5)
var g_lgt_color: texture_2d<f32>;

// "gTSpecularCol" in "clustered" in monolib/shader/shd_lgt.wishp.
@group(1) @binding(6)
var g_specular_color: texture_2d<f32>;

// PerDraw resources for each material ID type.
@group(2) @binding(0)
var<uniform> render_settings: RenderSettings;

// TODO: Create uniform arrays with max length 256 for these lights?
// TODO: Hardcode lighting from the character menu in xc3 for now?
// Uniform structs from program "clustered" from monolib/shader/shd_lgt.wishp.
struct PointLight {
    col: vec4<f32>,
    position: vec4<f32>,
    etc: vec4<f32>
}

struct SpotLight {
    col: vec4<f32>,
    position: vec4<f32>,
    etc: vec4<f32>,
    vector: vec4<f32>
}

struct ToonData {
    head_left_dir: vec4<f32>,
    head_up_dir: vec4<f32>,
    head_forward_dir: vec4<f32>,
}

// wgpu recommends @invariant for position with depth func equals.
struct VertexOutput {
    @builtin(position) @invariant position: vec4<f32>,
    @location(0) uv: vec2<f32>,
};

struct FragmentOutput {
    @location(0) color: vec4<f32>,
    @builtin(frag_depth) depth: f32
}

@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput {
    // A fullscreen triangle using index calculations.
    var out: VertexOutput;
    let x = f32((i32(in_vertex_index) << 1u) & 2);
    let y = f32(i32(in_vertex_index & 2u));
    out.position = vec4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
    out.uv = vec2(x, 1.0 - y);
    return out;
}

// Standard GGX BRDF in "clustered" in monolib/shader/shd_lgt.wishp.
fn ggx_brdf(roughness: f32, n_dot_h: f32) -> f32 {
    let a = roughness * roughness;
    let a2 = a * a;
    let pi = 3.14159;
    let n_dot_h2 = n_dot_h * n_dot_h;
    let denominator = (n_dot_h2) * (a2 - 1.0) + 1.0;
    return a2 / (pi * denominator * denominator);
}

fn mat_id_depth(id: u32) -> f32 {
    // Assume a Depth16 depth buffer.
    // wgpu maps [0.0, 1.0] to depth instead of [-1.0, 1.0].
    return f32(id + 1u) / 65535.0;
}

fn unpack_normal(g_normal: vec2<f32>) -> vec3<f32> {
    // Unpack the view space normals.
    let normal_x = g_normal.x * 2.0 - 1.0;
    let normal_y = g_normal.y * 2.0 - 1.0;
    let normal_z = sqrt(abs(1.0 - normal_x * normal_x - normal_y * normal_y));
    return vec3(normal_x, normal_y, normal_z);
}

// Adapted from "unbranch_to_depth" in xeno3/monolib/shader/shd_post.
fn unpack_etc_flags(etc_a: f32) -> u32 {
    // This is also the same logic used in the deferred shaders.
    return u32(etc_a * 255.0 + 0.1);
}

fn calculate_color(uv: vec2<f32>) -> vec4<f32> {
    let g_color = textureSample(g_color, shared_sampler, uv);
    let g_etc_buffer = textureSample(g_etc_buffer, shared_sampler, uv);
    let g_normal = textureSample(g_normal, shared_sampler, uv);
    let g_velocity = textureSample(g_velocity, shared_sampler, uv);
    let g_depth = textureSample(g_depth, shared_sampler, uv);
    let g_lgt_color = textureSample(g_lgt_color, shared_sampler, uv);

    let albedo = g_color.rgb;
    let metalness = g_etc_buffer.r;
    let glossiness = g_etc_buffer.g;
    
    // TODO: clamped using constant buffer?
    let roughness = clamp(1.0 - glossiness, 0.04, 0.995);

    let normal = unpack_normal(g_normal.xy);
    let ambient_occlusion = g_normal.z;

    var output = vec3(0.0);

    // Normals are in view space, so the view vector is simple.
    let view = vec3(0.0, 0.0, 1.0);
    let reflection = reflect(view, normal);

    let n_dot_v = max(dot(view, normal), 0.0);
    // TODO: Calculate this from lighting vectors.
    let n_dot_h = n_dot_v;
    
    // Basic lambertian diffuse for testing purposes.
    let diffuse_indirect = 0.35 * ambient_occlusion;
    let diffuse_direct = 1.0;
    let diffuse_lighting = mix(diffuse_indirect, diffuse_direct, n_dot_v);

    let ggx = ggx_brdf(roughness, n_dot_h);

    // TODO: ambient specular using BRDF map?
    let specular_lighting = ggx + 0.25;

    // TODO: fresnel?
    let f0 = mix(vec3(0.08), albedo, metalness);

    let k_specular = f0;
    let k_diffuse = 1.0 - metalness;

    output = albedo * k_diffuse * diffuse_lighting + specular_lighting * k_specular * ambient_occlusion;
    output += g_lgt_color.rgb;

    return vec4(output, 1.0);
}

fn calculate_toon_color(uv: vec2<f32>) -> vec4<f32> {
    let g_color = textureSample(g_color, shared_sampler, uv);
    let g_etc_buffer = textureSample(g_etc_buffer, shared_sampler, uv);
    let g_normal = textureSample(g_normal, shared_sampler, uv);
    let g_velocity = textureSample(g_velocity, shared_sampler, uv);
    let g_depth = textureSample(g_depth, shared_sampler, uv);
    let g_specular_color = textureSample(g_specular_color, shared_sampler, uv);

    let albedo = g_color.rgb;
    let metalness = g_etc_buffer.r; // TODO: different for toon?
    let glossiness = g_etc_buffer.g;

    let flags = unpack_etc_flags(g_etc_buffer.a);

    // Adapted from xeno3/chr/ch/ch11021013.pcsmt, shd00036, createBitInfo,
    let b_specular_col = (flags & (1u << 4u)) != 0u;
    
    // TODO: clamped using constant buffer?
    let roughness = clamp(1.0 - glossiness, 0.04, 0.995);

    let normal = unpack_normal(g_normal.xy);
    let ambient_occlusion = g_normal.z;

    // Normals are in view space, so the view vector is simple.
    let view = vec3(0.0, 0.0, 1.0);
    let reflection = reflect(view, normal);

    let n_dot_v = max(dot(view, normal), 0.0);
    // TODO: Calculate this from lighting vectors.
    let n_dot_h = n_dot_v;
    
    // Basic lambertian diffuse for testing purposes.
    // TODO: Toon shading in game has many more parameters.
    let diffuse_indirect = 0.25 * ambient_occlusion;
    let diffuse_direct = 1.0;
    let diffuse_lighting = mix(diffuse_indirect, diffuse_direct, n_dot_v);

    // TODO: Adding the toon shift only applies to xc3?
    let toon_shift_u = g_depth.w * 2.0 - 1.0;
    let toon_u = diffuse_lighting;
    let toon_v = toon_grad_v(g_etc_buffer.z);

    // TODO: Correctly use both gradients to fix massive melee mythra hair.
    // TODO: Are these the right gradients?
    let toon_diffuse = textureSample(g_toon_grad, shared_sampler, vec2(toon_u, toon_v)).rgb;

    var output = albedo * toon_diffuse;

    // TODO: is this the correct check for xc2 and xc3?
    if b_specular_col {
        // TODO: Does toon shading use ggx?
        // TODO: ambient specular using BRDF map?
        let ggx = ggx_brdf(roughness, n_dot_h);
        let toon_specular = ggx;

        // TODO: What is the default specular color?
        let specular_color = g_specular_color.rgb;

        // TODO: fresnel?
        let f0 = mix(specular_color, albedo, metalness);
        // TODO: specular intensity when using specular map?
        let k_specular = f0;

        let k_diffuse = 1.0 - metalness;

        output = output * k_diffuse + toon_specular * k_specular * ambient_occlusion;
    }

    return vec4(output, 1.0);
}

// TODO: entry points for each of the mat id types in game.
// 0, ouroboros core?
// 1, PBR
// 2, TOON
// 3, vegetation and colony 9 tents (sss?) also bathtub from xc2?
// 4, core crystal and levialord fins?
// 5, HAIR (xc3 only)

// Adapted from slct 0 nvsd 8 in xeno3/monolib/shader/shd_lgt.wishp.
fn toon_grad_v(etc_z: f32) -> f32 {
    // etc.z is often stored as row / 255.0 in the material work values.
    // Recover the row using round(etc.z * 255.0).
    let row_index = floor(etc_z * 255.0 + 0.5);
    // Select the center of a row from the 256x256 toon gradients texture.
    return (row_index + 0.5) / 256.0;
}

// Each material type is "masked" using depth function equals.
@fragment
fn fs_main(in: VertexOutput) -> FragmentOutput {
    var out: FragmentOutput;
    out.color = calculate_color(in.uv);
    out.depth = mat_id_depth(render_settings.mat_id);
    return out;
}

@fragment
fn fs_toon(in: VertexOutput) -> FragmentOutput {
    var out: FragmentOutput;
    out.color = calculate_toon_color(in.uv);
    out.depth = mat_id_depth(render_settings.mat_id);
    return out;
}

@fragment
fn fs_debug(in: VertexOutput) -> FragmentOutput {
    let g_color = textureSample(g_color, shared_sampler, in.uv);
    let g_etc_buffer = textureSample(g_etc_buffer, shared_sampler, in.uv);
    let g_normal = textureSample(g_normal, shared_sampler, in.uv);
    let g_velocity = textureSample(g_velocity, shared_sampler, in.uv);
    let g_depth = textureSample(g_depth, shared_sampler, in.uv);
    let g_lgt_color = textureSample(g_lgt_color, shared_sampler, in.uv);
    let g_specular_color = textureSample(g_specular_color, shared_sampler, in.uv);

    var color = vec4(0.0);
    switch (debug_settings.render_mode) {
        case 1u: {
            color = g_color;
        }
        case 2u: {
            color = g_etc_buffer;
        }
        case 3u: {
            color = g_normal;
        }
        case 4u: {
            color = g_velocity;
        }
        case 5u: {
            color = g_depth;
        }
        case 6u: {
            color = g_lgt_color;
        }
        case 7u: {
            color = g_specular_color;
        }
        default: {
            color = vec4(0.0);
        }
    }

    var out: FragmentOutput;
    switch (debug_settings.channel) {
        case 0: {
            out.color = vec4(color.rrr, 1.0);
        }
        case 1: {
            out.color = vec4(color.ggg, 1.0);
        }
        case 2: {
            out.color = vec4(color.bbb, 1.0);
        }
        case 3: {
            out.color = vec4(color.aaa, 1.0);
        }
        default: {
            out.color = color;
        }
    }

    return out;
}