Files
retro3d/shaders/retro3d.frag.msl
2025-12-13 16:13:13 -06:00

168 lines
4.6 KiB
Plaintext

#include <metal_stdlib>
using namespace metal;
struct Uniforms {
float4x4 mvp;
float4x4 model;
float4x4 view;
float4x4 projection;
float4 ambient; // rgb, unused
float4 light_dir; // xyz normalized, unused
float4 light_color; // rgb, intensity
float4 fog_params; // near, far, unused, enabled
float4 fog_color; // rgb, unused
float4 tint; // rgba (base_color_factor from glTF)
float4 style_params; // style_id, vertex_snap, affine, dither
float4 resolution; // w, h, unused, unused
float4 material_params; // alpha_mode (0=opaque,1=mask,2=blend), alpha_cutoff, unlit, unused
};
struct FragmentIn {
float4 position [[position]];
float3 world_normal;
float2 uv;
float4 color;
float fog_factor;
float3 noperspective_uv;
};
// Dither pattern for Saturn style (4x4 Bayer matrix)
constant float dither_matrix[16] = {
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
};
float get_dither(float2 pos) {
int x = int(pos.x) % 4;
int y = int(pos.y) % 4;
return dither_matrix[y * 4 + x];
}
// Quantize color to lower bit depth
float3 quantize_color(float3 color, float bits) {
float levels = pow(2.0, bits) - 1.0;
return floor(color * levels + 0.5) / levels;
}
fragment float4 fragment_main(
FragmentIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]]
) {
float style_id = uniforms.style_params.x;
float affine = uniforms.style_params.z;
float dither = uniforms.style_params.w;
// Material params
float alpha_mode = uniforms.material_params.x; // 0=opaque, 1=mask, 2=blend
float alpha_cutoff = uniforms.material_params.y; // default 0.5
float unlit = uniforms.material_params.z;
// Get UV coordinates
float2 uv;
if (affine > 0.5) {
// Affine texturing (PS1 style) - divide by interpolated w
uv = in.noperspective_uv.xy / in.noperspective_uv.z;
} else {
uv = in.uv;
}
// Sample texture
float4 tex_color = tex.sample(samp, uv);
// glTF spec: final color = vertexColor * baseColorFactor * baseColorTexture
// tint = base_color_factor from material
float4 base_color = in.color * uniforms.tint * tex_color;
// Alpha handling based on alpha_mode
float alpha = base_color.a;
// MASK mode: discard fragments below cutoff
if (alpha_mode > 0.5 && alpha_mode < 1.5) {
if (alpha < alpha_cutoff) {
discard_fragment();
}
alpha = 1.0; // MASK mode outputs fully opaque or discards
}
// OPAQUE mode: ignore alpha entirely
if (alpha_mode < 0.5) {
alpha = 1.0;
}
// BLEND mode: alpha is used as-is (alpha_mode >= 1.5)
float3 final_color;
if (unlit > 0.5) {
// Unlit material - no lighting calculation
final_color = base_color.rgb;
} else {
// Lighting calculation
float3 normal = normalize(in.world_normal);
float3 light_dir = normalize(uniforms.light_dir.xyz);
float ndotl = max(dot(normal, light_dir), 0.0);
float3 ambient = uniforms.ambient.rgb;
float3 diffuse = uniforms.light_color.rgb * uniforms.light_color.w * ndotl;
float3 lighting = ambient + diffuse;
// Apply lighting
final_color = base_color.rgb * lighting;
}
// Style-specific processing
if (style_id < 0.5) {
// PS1 style: 15-bit color (5 bits per channel)
final_color = quantize_color(final_color, 5.0);
} else if (style_id < 1.5) {
// N64 style: smoother, 16-bit color with bilinear filtering
// (filtering is handled by sampler, just quantize slightly)
final_color = quantize_color(final_color, 5.0);
} else {
// Saturn style: dithered, flat shaded look
if (dither > 0.5) {
float d = get_dither(in.position.xy);
// Add dither before quantization
final_color += (d - 0.5) * 0.1;
}
final_color = quantize_color(final_color, 5.0);
}
// Apply fog
float3 fog_color = uniforms.fog_color.rgb;
final_color = mix(fog_color, final_color, in.fog_factor);
return float4(final_color, alpha);
}
// Unlit fragment shader for sprites/UI
fragment float4 fragment_unlit(
FragmentIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(1)]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]]
) {
float affine = uniforms.style_params.z;
float2 uv;
if (affine > 0.5) {
uv = in.noperspective_uv.xy / in.noperspective_uv.z;
} else {
uv = in.uv;
}
float4 tex_color = tex.sample(samp, uv);
float4 color = in.color * tex_color * uniforms.tint;
// Alpha test for sprites
if (color.a < 0.1) {
discard_fragment();
}
return color;
}