Files
SDL3CPlusPlus/scripts/shader_toolkit.lua

1609 lines
46 KiB
Lua

local shader_toolkit = {}
local path_sep = package.config:sub(1, 1)
local is_windows = path_sep == "\\"
local function quote_arg(value)
return string.format("%q", value)
end
local function execute_command(command)
local ok, exit_type, code = os.execute(command)
if ok == true then
return true
end
if type(ok) == "number" then
return ok == 0
end
return exit_type == "exit" and code == 0
end
local function ensure_directory(path)
if not path or path == "" or path == "." then
return true
end
local command
if is_windows then
command = string.format("mkdir %s >NUL 2>&1", quote_arg(path))
else
command = string.format("mkdir -p %s >/dev/null 2>&1", quote_arg(path))
end
return execute_command(command)
end
local function get_directory(path)
return path:match("^(.*)[/\\\\]") or ""
end
local function path_join(...)
local parts = {...}
local result = ""
for index, part in ipairs(parts) do
if part and part ~= "" then
if result ~= "" and result:sub(-1) ~= path_sep then
result = result .. path_sep
end
if index > 1 and part:sub(1, 1) == path_sep then
part = part:sub(2)
end
result = result .. part
end
end
return result
end
local function is_absolute_path(path)
if not path or path == "" then
return false
end
if path:sub(1, 1) == "/" then
return true
end
return is_windows and path:match("^%a:[/\\\\]") ~= nil
end
local function file_exists(path)
local file = io.open(path, "rb")
if file then
file:close()
return true
end
return false
end
local function normalize_output_name(name)
if not name or name == "" then
return nil
end
if name:match("%.vert$") then
return name:gsub("%.vert$", "")
end
if name:match("%.frag$") then
return name:gsub("%.frag$", "")
end
return name
end
local function get_module_directory()
if not debug or not debug.getinfo then
return nil
end
local info = debug.getinfo(1, "S")
local source = info and info.source or ""
if source:sub(1, 1) == "@" then
source = source:sub(2)
end
return get_directory(source)
end
local function resolve_output_dir(output_dir)
if output_dir and output_dir ~= "" then
return output_dir
end
local shader_dir = "shaders"
local project_root = nil
if type(config) == "table" then
shader_dir = config.shaders_directory or shader_dir
project_root = config.project_root
end
if is_absolute_path(shader_dir) then
return shader_dir
end
local base_dir = get_module_directory() or "."
if project_root and project_root ~= "" then
if is_absolute_path(project_root) then
base_dir = project_root
else
base_dir = path_join(base_dir, project_root)
end
else
base_dir = path_join(base_dir, "..")
end
return path_join(base_dir, shader_dir)
end
local function find_in_path(command)
local check_command
if is_windows then
check_command = "where " .. command .. " 2>NUL"
else
check_command = "command -v " .. command .. " 2>/dev/null"
end
local handle = io.popen(check_command)
if not handle then
return false
end
local output = handle:read("*a") or ""
handle:close()
return output ~= ""
end
local function detect_compiler()
local candidates = {"glslangValidator", "glslc"}
for _, candidate in ipairs(candidates) do
if find_in_path(candidate) then
return candidate
end
end
return nil
end
local function compile_shader(compiler, input_path, output_path)
local quoted_compiler = quote_arg(compiler)
local quoted_input = quote_arg(input_path)
local quoted_output = quote_arg(output_path)
local command
if compiler:find("glslangValidator") then
command = string.format("%s -V %s -o %s", quoted_compiler, quoted_input, quoted_output)
else
command = string.format("%s %s -o %s", quoted_compiler, quoted_input, quoted_output)
end
if not execute_command(command) then
error("Shader compilation failed: " .. command)
end
end
local function write_text_file(path, contents)
local directory = get_directory(path)
if directory ~= "" and not ensure_directory(directory) then
error("Failed to create directory: " .. directory)
end
local file = io.open(path, "w")
if not file then
error("Failed to open file for writing: " .. path)
end
file:write(contents)
file:close()
end
local function read_text_file(path)
local file = io.open(path, "rb")
if not file then
return nil
end
local contents = file:read("*a")
file:close()
return contents
end
local function write_text_file_if_changed(path, contents)
local existing = read_text_file(path)
if existing == contents then
return
end
write_text_file(path, contents)
end
local function normalize_number(value, fallback)
local number_value = tonumber(value)
if number_value == nil then
return fallback
end
return number_value
end
local function normalize_color(color, fallback)
local default = fallback or {1.0, 1.0, 1.0, 1.0}
if type(color) ~= "table" then
return {default[1], default[2], default[3], default[4]}
end
local r = normalize_number(color[1], default[1])
local g = normalize_number(color[2], default[2])
local b = normalize_number(color[3], default[3])
local a = normalize_number(color[4], default[4])
return {r, g, b, a}
end
local function normalize_vec3(value, fallback)
local default = fallback or {1.0, 1.0, 1.0}
if type(value) ~= "table" then
return {default[1], default[2], default[3]}
end
return {
normalize_number(value[1], default[1]),
normalize_number(value[2], default[2]),
normalize_number(value[3], default[3]),
}
end
local function format_vec3(value)
return string.format("vec3(%.3f, %.3f, %.3f)", value[1], value[2], value[3])
end
local function merge_tables(base, overrides)
local merged = {}
if type(base) == "table" then
for key, value in pairs(base) do
merged[key] = value
end
end
if type(overrides) == "table" then
for key, value in pairs(overrides) do
merged[key] = value
end
end
return merged
end
local ShaderTemplate = {}
ShaderTemplate.__index = ShaderTemplate
function ShaderTemplate:new(name, generator, defaults)
if type(name) ~= "string" or name == "" then
error("Template name must be a non-empty string")
end
if type(generator) ~= "function" then
error("Template generator must be a function")
end
local instance = {
name = name,
generator = generator,
defaults = defaults or {},
}
return setmetatable(instance, ShaderTemplate)
end
function ShaderTemplate:Generate(options)
local merged = merge_tables(self.defaults, options)
return self.generator(merged)
end
local ShaderVariant = {}
ShaderVariant.__index = ShaderVariant
function ShaderVariant:new(options)
if type(options) ~= "table" then
error("Shader variant options must be a table")
end
local instance = {
key = options.key,
template = options.template or options.template_name,
output_name = options.output_name or options.outputName,
output_dir = options.output_dir or options.outputDir,
output_mode = options.output_mode or options.outputMode,
compile = options.compile,
compiler = options.compiler,
skip_if_present = options.skip_if_present,
parameters = options.parameters or options.params,
}
return setmetatable(instance, ShaderVariant)
end
local push_constants_block = [[
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 viewProj;
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pushConstants;
]]
local vertex_color_source = [[
#version 450
layout(location = 0) in vec3 inPos;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
]] .. push_constants_block .. [[
void main() {
fragColor = inColor;
gl_Position = pushConstants.viewProj * pushConstants.model * vec4(inPos, 1.0);
}
]]
local vertex_color_fragment_source = [[
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
]]
local gui_2d_vertex_source = [[
#version 450
layout(location = 0) in vec3 inPos;
layout(location = 1) in vec4 inColor;
layout(location = 0) out vec4 fragColor;
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 viewProj;
// Extended fields for PBR/atmospherics (ignored by basic shaders)
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
// Atmospherics parameters
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pushConstants;
void main() {
fragColor = inColor;
vec4 worldPos = pushConstants.model * vec4(inPos, 1.0);
gl_Position = pushConstants.viewProj * worldPos;
}
]]
local gui_2d_fragment_source = [[
#version 450
layout(location = 0) in vec4 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = fragColor;
}
]]
local shadow_vertex_source = [[
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
// layout(location = 2) in vec2 inTexCoord; // Not used
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 viewProj;
// Extended fields for PBR/atmospherics
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
// Atmospherics parameters
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pc;
void main() {
gl_Position = pc.lightViewProj * pc.model * vec4(inPosition, 1.0);
}
]]
local shadow_fragment_source = [[
#version 450
void main() {
// Empty fragment shader for shadow mapping
// Depth is automatically written
}
]]
local fullscreen_vertex_source = [[
#version 450
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec2 inTexCoord;
layout(location = 0) out vec2 fragPosition;
layout(location = 1) out vec2 fragTexCoord;
void main() {
fragPosition = inPosition;
fragTexCoord = inTexCoord;
gl_Position = vec4(inPosition, 0.0, 1.0);
}
]]
local ssgi_fragment_source = [[
#version 450
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec2 inTexCoord;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D sceneColor;
layout(set = 0, binding = 1) uniform sampler2D normalBuffer;
layout(set = 0, binding = 2) uniform sampler2D depthBuffer;
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 viewProj;
// Extended fields for PBR/atmospherics
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
// Atmospherics parameters
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pc;
const int NUM_SAMPLES = 16;
const float SAMPLE_RADIUS = 0.5;
// Reconstruct world position from depth
vec3 worldPosFromDepth(float depth, vec2 texCoord) {
vec4 clipSpace = vec4(texCoord * 2.0 - 1.0, depth, 1.0);
vec4 viewSpace = pc.proj * clipSpace; // Use proj as invProj for now (dummy)
viewSpace /= viewSpace.w;
vec4 worldSpace = pc.view * viewSpace; // Use view as invView for now (dummy)
return worldSpace.xyz;
}
vec3 ssao(vec2 texCoord) {
float depth = texture(depthBuffer, texCoord).r;
if (depth >= 1.0) return vec3(1.0); // Skybox
vec3 worldPos = worldPosFromDepth(depth, texCoord);
vec3 normal = normalize(texture(normalBuffer, texCoord).rgb * 2.0 - 1.0);
float occlusion = 0.0;
for (int i = 0; i < NUM_SAMPLES; i++) {
// Generate sample position in hemisphere around normal
float angle = (float(i) / float(NUM_SAMPLES)) * 6.283185;
vec2 offset = vec2(cos(angle), sin(angle)) * SAMPLE_RADIUS;
vec2 sampleTexCoord = texCoord + offset / textureSize(depthBuffer, 0);
float sampleDepth = texture(depthBuffer, sampleTexCoord).r;
if (sampleDepth < depth - 0.01) { // Occluded
occlusion += 1.0;
}
}
occlusion = 1.0 - (occlusion / float(NUM_SAMPLES));
return vec3(occlusion);
}
void main() {
vec3 sceneColor = texture(sceneColor, inTexCoord).rgb;
vec3 ao = ssao(inTexCoord);
// Apply ambient occlusion
vec3 finalColor = sceneColor * (0.3 + 0.7 * ao); // Mix AO with direct lighting
outColor = vec4(finalColor, 1.0);
}
]]
local volumetric_fragment_source = [[
#version 450
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec2 inTexCoord;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D sceneColor;
layout(set = 0, binding = 1) uniform sampler2D depthBuffer;
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 viewProj;
// Extended fields for PBR/atmospherics
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
// Atmospherics parameters
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pc;
const int NUM_SAMPLES = 100;
const float DECAY_BASE = 0.96815;
const float WEIGHT_BASE = 0.58767;
const float EXPOSURE = 0.2;
void main() {
vec2 texCoord = inTexCoord;
vec2 lightScreenPos = vec2(0.5, 0.5); // Dummy light position
vec2 deltaTexCoord = (texCoord - lightScreenPos);
deltaTexCoord *= 1.0 / float(NUM_SAMPLES) * 0.5; // Scale for effect
vec3 color = texture(sceneColor, texCoord).rgb;
// Only apply god rays if we're looking towards the light
float centerDistance = length(lightScreenPos - vec2(0.5, 0.5));
if (centerDistance < 0.8) { // Light is visible on screen
vec3 godRayColor = vec3(0.0);
float weight = WEIGHT_BASE;
for (int i = 0; i < NUM_SAMPLES; i++) {
texCoord -= deltaTexCoord;
vec3 sampleColor = texture(sceneColor, texCoord).rgb;
godRayColor += sampleColor * weight;
weight *= DECAY_BASE;
}
color += godRayColor * EXPOSURE * 1.0; // Dummy intensity
}
outColor = vec4(color, 1.0);
}
]]
local vertex_world_color_source = [[
#version 450
layout(location = 0) in vec3 inPos;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec3 fragWorldPos;
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 viewProj;
// Extended fields for PBR/atmospherics (ignored by basic shaders)
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
// Atmospherics parameters
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pushConstants;
void main() {
fragColor = inColor;
vec4 worldPos = pushConstants.model * vec4(inPos, 1.0);
fragWorldPos = worldPos.xyz;
gl_Position = pushConstants.viewProj * worldPos;
}
]]
local function build_cube_rainbow_fragment_source(options)
local band_scale = normalize_number(options.band_scale, 0.35)
local diagonal_scale = normalize_number(options.diagonal_scale, 0.25)
local mix_factor = normalize_number(options.mix_factor, 0.08)
local ambient_strength = normalize_number(options.ambient_strength, 0.28)
local key_intensity = normalize_number(options.key_intensity, 0.9)
local fill_intensity = normalize_number(options.fill_intensity, 0.4)
local key_dir = format_vec3(normalize_vec3(options.key_dir, {-0.35, 1.0, -0.25}))
local fill_dir = format_vec3(normalize_vec3(options.fill_dir, {0.45, 0.6, 0.2}))
return string.format([[
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 0) out vec4 outColor;
vec3 ComputeNormal() {
vec3 dx = dFdx(fragWorldPos);
vec3 dy = dFdy(fragWorldPos);
return normalize(cross(dx, dy));
}
vec3 RainbowBand(float t) {
t = fract(t);
float scaled = t * 5.0;
int index = int(floor(scaled));
float blend = fract(scaled);
vec3 red = vec3(0.91, 0.19, 0.21);
vec3 orange = vec3(0.99, 0.49, 0.09);
vec3 yellow = vec3(0.99, 0.86, 0.22);
vec3 green = vec3(0.16, 0.74, 0.39);
vec3 blue = vec3(0.24, 0.48, 0.88);
vec3 purple = vec3(0.56, 0.25, 0.75);
vec3 a = red;
vec3 b = orange;
if (index == 1) {
a = orange;
b = yellow;
} else if (index == 2) {
a = yellow;
b = green;
} else if (index == 3) {
a = green;
b = blue;
} else if (index == 4) {
a = blue;
b = purple;
} else if (index >= 5) {
a = purple;
b = purple;
}
return mix(a, b, blend);
}
void main() {
float bandPos = fragWorldPos.y * %.3f;
float diagonal = (fragWorldPos.x + fragWorldPos.z) * %.3f;
vec3 rainbow = RainbowBand(bandPos + diagonal);
vec3 baseColor = mix(rainbow, fragColor, %.3f);
vec3 normal = ComputeNormal();
vec3 lighting = vec3(0.0);
vec3 keyDir = normalize(%s);
vec3 fillDir = normalize(%s);
float keyNdotL = abs(dot(normal, keyDir));
float fillNdotL = abs(dot(normal, fillDir));
lighting += vec3(1.0, 0.92, 0.85) * keyNdotL * %.3f;
lighting += vec3(0.35, 0.45, 0.65) * fillNdotL * %.3f;
vec3 finalColor = baseColor * (%.3f + lighting);
outColor = vec4(clamp(finalColor, 0.0, 1.0), 1.0);
}
]], band_scale, diagonal_scale, mix_factor, key_dir, fill_dir, key_intensity, fill_intensity,
ambient_strength)
end
local function build_solid_lit_fragment_source(options)
local light_color = format_vec3(normalize_vec3(options.light_color, {1.0, 0.9, 0.6}))
local light_intensity = normalize_number(options.light_intensity, 0.9)
local ambient_strength = normalize_number(options.ambient_strength, 0.22)
return string.format([[
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 0) out vec4 outColor;
// Lantern positions (8 lights)
const vec3 LIGHT_POSITIONS[8] = vec3[8](
vec3(13.0, 4.5, 13.0), // Corner
vec3(-13.0, 4.5, 13.0), // Corner
vec3(13.0, 4.5, -13.0), // Corner
vec3(-13.0, 4.5, -13.0), // Corner
vec3(0.0, 4.5, 13.0), // Wall midpoint
vec3(0.0, 4.5, -13.0), // Wall midpoint
vec3(13.0, 4.5, 0.0), // Wall midpoint
vec3(-13.0, 4.5, 0.0) // Wall midpoint
);
const vec3 LIGHT_COLOR = %s; // Warm lantern color
const float LIGHT_INTENSITY = %.3f;
const float AMBIENT_STRENGTH = %.3f; // Boost ambient to preserve surface color
float calculateAttenuation(float distance) {
// Quadratic attenuation: 1 / (constant + linear*d + quadratic*d^2)
const float kConstant = 1.0;
const float kLinear = 0.09;
const float kQuadratic = 0.032;
return 1.0 / (kConstant + kLinear * distance + kQuadratic * distance * distance);
}
void main() {
vec3 ambient = AMBIENT_STRENGTH * fragColor;
vec3 lighting = vec3(0.0);
// Calculate contribution from each lantern
for (int i = 0; i < 8; i++) {
vec3 lightDir = LIGHT_POSITIONS[i] - fragWorldPos;
float distance = length(lightDir);
lightDir = normalize(lightDir);
// Distance attenuation
float attenuation = calculateAttenuation(distance);
// Add light contribution
lighting += LIGHT_COLOR * LIGHT_INTENSITY * attenuation;
}
// Combine ambient and dynamic lighting with surface color
vec3 finalColor = ambient + fragColor * lighting;
// Clamp to prevent over-bright areas
finalColor = clamp(finalColor, 0.0, 1.0);
outColor = vec4(finalColor, 1.0);
}
]], light_color, light_intensity, ambient_strength)
end
local function build_floor_fragment_source(options)
local surface_base = format_vec3(normalize_vec3(options.surface_base, {0.02, 0.95, 0.72}))
local checker_scale = normalize_number(options.checker_scale, 0.55)
local light_color = format_vec3(normalize_vec3(options.light_color, {1.0, 0.92, 0.7}))
local light_intensity = normalize_number(options.light_intensity, 1.0)
local ambient_strength = normalize_number(options.ambient_strength, 0.3)
return string.format([[
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 0) out vec4 outColor;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
const vec3 SURFACE_BASE = %s;
vec3 ComputeNormal() {
vec3 dx = dFdx(fragWorldPos);
vec3 dy = dFdy(fragWorldPos);
return normalize(cross(dx, dy));
}
const vec3 LIGHT_POSITIONS[8] = vec3[8](
vec3(13.0, 4.5, 13.0),
vec3(-13.0, 4.5, 13.0),
vec3(13.0, 4.5, -13.0),
vec3(-13.0, 4.5, -13.0),
vec3(0.0, 4.5, 13.0),
vec3(0.0, 4.5, -13.0),
vec3(13.0, 4.5, 0.0),
vec3(-13.0, 4.5, 0.0)
);
const vec3 LIGHT_COLOR = %s;
const float LIGHT_INTENSITY = %.3f;
const float AMBIENT_STRENGTH = %.3f;
float calculateAttenuation(float distance) {
const float kConstant = 1.0;
const float kLinear = 0.09;
const float kQuadratic = 0.032;
return 1.0 / (kConstant + kLinear * distance + kQuadratic * distance * distance);
}
void main() {
vec3 baseColor = SURFACE_BASE;
float checkerScale = %.3f;
float cx = step(0.5, fract(fragWorldPos.x * checkerScale));
float cz = step(0.5, fract(fragWorldPos.z * checkerScale));
float checker = abs(cx - cz);
float grit = hash(floor(fragWorldPos.xz * 2.2));
float pattern = mix(0.82, 1.08, checker) * mix(0.96, 1.04, grit);
baseColor *= pattern;
vec3 normal = ComputeNormal();
vec3 ambient = AMBIENT_STRENGTH * baseColor;
vec3 lighting = vec3(0.0);
for (int i = 0; i < 8; i++) {
vec3 lightDir = LIGHT_POSITIONS[i] - fragWorldPos;
float distance = length(lightDir);
lightDir = normalize(lightDir);
float attenuation = calculateAttenuation(distance);
float ndotl = abs(dot(normal, lightDir));
lighting += LIGHT_COLOR * LIGHT_INTENSITY * attenuation * ndotl;
}
vec3 keyDir = normalize(vec3(-0.3, 1.0, -0.4));
float keyNdotL = abs(dot(normal, keyDir));
lighting += vec3(0.9, 0.95, 1.0) * keyNdotL * 0.25;
vec3 finalColor = ambient + baseColor * lighting;
finalColor = clamp(finalColor, 0.0, 1.0);
outColor = vec4(finalColor, 1.0);
}
]], surface_base, light_color, light_intensity, ambient_strength, checker_scale)
end
local function build_wall_fragment_source(options)
local surface_base = format_vec3(normalize_vec3(options.surface_base, {0.98, 0.18, 0.08}))
local plank_scale = normalize_number(options.plank_scale, 0.4)
local light_color = format_vec3(normalize_vec3(options.light_color, {1.0, 0.88, 0.6}))
local light_intensity = normalize_number(options.light_intensity, 0.95)
local ambient_strength = normalize_number(options.ambient_strength, 0.24)
return string.format([[
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 0) out vec4 outColor;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(91.7, 127.3))) * 43758.5453123);
}
const vec3 SURFACE_BASE = %s;
vec3 ComputeNormal() {
vec3 dx = dFdx(fragWorldPos);
vec3 dy = dFdy(fragWorldPos);
return normalize(cross(dx, dy));
}
const vec3 LIGHT_POSITIONS[8] = vec3[8](
vec3(13.0, 4.5, 13.0),
vec3(-13.0, 4.5, 13.0),
vec3(13.0, 4.5, -13.0),
vec3(-13.0, 4.5, -13.0),
vec3(0.0, 4.5, 13.0),
vec3(0.0, 4.5, -13.0),
vec3(13.0, 4.5, 0.0),
vec3(-13.0, 4.5, 0.0)
);
const vec3 LIGHT_COLOR = %s;
const float LIGHT_INTENSITY = %.3f;
const float AMBIENT_STRENGTH = %.3f;
float calculateAttenuation(float distance) {
const float kConstant = 1.0;
const float kLinear = 0.09;
const float kQuadratic = 0.032;
return 1.0 / (kConstant + kLinear * distance + kQuadratic * distance * distance);
}
void main() {
vec3 baseColor = SURFACE_BASE;
float axisSelector = step(abs(fragWorldPos.z), abs(fragWorldPos.x));
float coord = mix(fragWorldPos.z, fragWorldPos.x, axisSelector);
float plankScale = %.3f;
float plank = abs(fract(coord * plankScale) - 0.5);
float groove = smoothstep(0.46, 0.5, plank);
float grain = hash(floor(vec2(coord * 1.2, fragWorldPos.y * 2.0)));
baseColor *= mix(0.92, 1.05, grain);
baseColor *= mix(1.0, 0.78, groove);
vec3 normal = ComputeNormal();
vec3 ambient = AMBIENT_STRENGTH * baseColor;
vec3 lighting = vec3(0.0);
for (int i = 0; i < 8; i++) {
vec3 lightDir = LIGHT_POSITIONS[i] - fragWorldPos;
float distance = length(lightDir);
lightDir = normalize(lightDir);
float attenuation = calculateAttenuation(distance);
float ndotl = abs(dot(normal, lightDir));
lighting += LIGHT_COLOR * LIGHT_INTENSITY * attenuation * ndotl;
}
vec3 keyDir = normalize(vec3(0.35, 0.9, 0.2));
float keyNdotL = abs(dot(normal, keyDir));
lighting += vec3(1.0, 0.9, 0.8) * keyNdotL * 0.2;
vec3 finalColor = ambient + baseColor * lighting;
finalColor = clamp(finalColor, 0.0, 1.0);
outColor = vec4(finalColor, 1.0);
}
]], surface_base, light_color, light_intensity, ambient_strength, plank_scale)
end
local function build_ceiling_fragment_source(options)
local surface_base = format_vec3(normalize_vec3(options.surface_base, {0.98, 0.96, 0.1}))
local grid_scale = normalize_number(options.grid_scale, 0.45)
local light_color = format_vec3(normalize_vec3(options.light_color, {0.92, 0.96, 1.0}))
local light_intensity = normalize_number(options.light_intensity, 0.85)
local ambient_strength = normalize_number(options.ambient_strength, 0.38)
return string.format([[
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 0) out vec4 outColor;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(63.1, 157.9))) * 43758.5453123);
}
const vec3 SURFACE_BASE = %s;
vec3 ComputeNormal() {
vec3 dx = dFdx(fragWorldPos);
vec3 dy = dFdy(fragWorldPos);
return normalize(cross(dx, dy));
}
const vec3 LIGHT_POSITIONS[8] = vec3[8](
vec3(13.0, 4.5, 13.0),
vec3(-13.0, 4.5, 13.0),
vec3(13.0, 4.5, -13.0),
vec3(-13.0, 4.5, -13.0),
vec3(0.0, 4.5, 13.0),
vec3(0.0, 4.5, -13.0),
vec3(13.0, 4.5, 0.0),
vec3(-13.0, 4.5, 0.0)
);
const vec3 LIGHT_COLOR = %s;
const float LIGHT_INTENSITY = %.3f;
const float AMBIENT_STRENGTH = %.3f;
float calculateAttenuation(float distance) {
const float kConstant = 1.0;
const float kLinear = 0.09;
const float kQuadratic = 0.032;
return 1.0 / (kConstant + kLinear * distance + kQuadratic * distance * distance);
}
void main() {
vec3 baseColor = SURFACE_BASE;
vec2 gridUv = fragWorldPos.xz * %.3f;
vec2 grid = abs(fract(gridUv) - 0.5);
float gridLine = step(0.48, max(grid.x, grid.y));
float speckle = hash(floor(fragWorldPos.xz * 3.0));
baseColor *= mix(0.94, 1.04, speckle);
baseColor *= mix(1.0, 0.84, gridLine);
vec3 normal = ComputeNormal();
vec3 ambient = AMBIENT_STRENGTH * baseColor;
vec3 lighting = vec3(0.0);
for (int i = 0; i < 8; i++) {
vec3 lightDir = LIGHT_POSITIONS[i] - fragWorldPos;
float distance = length(lightDir);
lightDir = normalize(lightDir);
float attenuation = calculateAttenuation(distance);
float ndotl = abs(dot(normal, lightDir));
lighting += LIGHT_COLOR * LIGHT_INTENSITY * attenuation * ndotl;
}
vec3 keyDir = normalize(vec3(-0.15, 1.0, 0.25));
float keyNdotL = abs(dot(normal, keyDir));
lighting += vec3(0.85, 0.95, 1.0) * keyNdotL * 0.28;
vec3 finalColor = ambient + baseColor * lighting;
finalColor = clamp(finalColor, 0.0, 1.0);
outColor = vec4(finalColor, 1.0);
}
]], surface_base, light_color, light_intensity, ambient_strength, grid_scale)
end
local pbr_vertex_source = [[
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor; // Color instead of normal
layout(location = 2) in vec2 inTexCoord; // Not used for now
layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec3 fragWorldPos;
layout(location = 2) out vec3 fragNormal;
layout(location = 3) out vec2 fragTexCoord;
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 viewProj;
// Extended fields for PBR/atmospherics
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
// Atmospherics parameters
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pc;
void main() {
vec4 worldPos = pc.model * vec4(inPosition, 1.0);
gl_Position = pc.proj * pc.view * worldPos;
fragWorldPos = worldPos.xyz;
fragNormal = normalize(mat3(pc.model) * vec3(0.0, 0.0, 1.0)); // Simple normal for flat shading
fragTexCoord = vec2(0.0, 0.0); // Not used
fragColor = inColor; // Use vertex color
}
]]
local function build_pbr_fragment_source(options)
local material_albedo = format_vec3(normalize_vec3(options.material_albedo, {0.8, 0.8, 0.8}))
local material_roughness = normalize_number(options.material_roughness, 0.3)
local material_metallic = normalize_number(options.material_metallic, 0.1)
local light_color = format_vec3(normalize_vec3(options.light_color, {1.0, 0.9, 0.6}))
local light_intensity = normalize_number(options.light_intensity, 1.2)
return string.format([[
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 2) in vec3 fragNormal;
layout(location = 3) in vec2 fragTexCoord;
layout(location = 0) out vec4 outColor;
// Material properties
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 view;
mat4 proj;
mat4 lightViewProj;
vec3 cameraPos;
float time;
// Atmospherics parameters
float ambientStrength;
float fogDensity;
float fogStart;
float fogEnd;
vec3 fogColor;
float gamma;
float exposure;
int enableShadows;
int enableFog;
} pc;
// Lighting uniforms
// layout(set = 0, binding = 0) uniform sampler2D shadowMap;
// Material parameters (can be extended to use textures)
const vec3 MATERIAL_ALBEDO = %s;
const float MATERIAL_ROUGHNESS = %.3f;
const float MATERIAL_METALLIC = %.3f;
// Atmospheric parameters
// const vec3 FOG_COLOR = vec3(0.05, 0.05, 0.08);
// const float FOG_DENSITY = 0.003;
// const float AMBIENT_STRENGTH = 0.01; // Much darker ambient
// Light properties
const vec3 LIGHT_COLOR = %s;
const float LIGHT_INTENSITY = %.3f;
const vec3 LIGHT_POSITIONS[8] = vec3[8](
vec3(13.0, 4.5, 13.0),
vec3(-13.0, 4.5, 13.0),
vec3(13.0, 4.5, -13.0),
vec3(-13.0, 4.5, -13.0),
vec3(0.0, 4.5, 13.0),
vec3(0.0, 4.5, -13.0),
vec3(13.0, 4.5, 0.0),
vec3(-13.0, 4.5, 0.0)
);
// PBR functions
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
float num = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = 3.14159 * denom * denom;
return num / denom;
}
float GeometrySchlickGGX(float NdotV, float roughness) {
float r = (roughness + 1.0);
float k = (r * r) / 8.0;
float num = NdotV;
float denom = NdotV * (1.0 - k) + k;
return num / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
float calculateShadow(vec3 worldPos) {
// TODO: Implement shadow mapping
return 0.0; // No shadow for now
}
vec3 calculateLighting(vec3 worldPos, vec3 normal, vec3 viewDir) {
vec3 F0 = vec3(0.04);
F0 = mix(F0, MATERIAL_ALBEDO, MATERIAL_METALLIC);
vec3 Lo = vec3(0.0);
for (int i = 0; i < 8; i++) {
vec3 lightDir = normalize(LIGHT_POSITIONS[i] - worldPos);
vec3 halfway = normalize(viewDir + lightDir);
float distance = length(LIGHT_POSITIONS[i] - worldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = LIGHT_COLOR * LIGHT_INTENSITY * attenuation;
float NDF = DistributionGGX(normal, halfway, MATERIAL_ROUGHNESS);
float G = GeometrySmith(normal, viewDir, lightDir, MATERIAL_ROUGHNESS);
vec3 F = fresnelSchlick(max(dot(halfway, viewDir), 0.0), F0);
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - MATERIAL_METALLIC;
float NdotL = max(dot(normal, lightDir), 0.0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(normal, viewDir), 0.0) * NdotL + 0.0001;
vec3 specular = numerator / denominator;
Lo += (kD * MATERIAL_ALBEDO / 3.14159 + specular) * radiance * NdotL;
}
return Lo;
}
void main() {
vec3 normal = normalize(fragNormal);
vec3 viewDir = normalize(pc.cameraPos - fragWorldPos);
// Ambient lighting
vec3 ambient = pc.ambientStrength * MATERIAL_ALBEDO;
// Direct lighting with PBR
vec3 lighting = calculateLighting(fragWorldPos, normal, viewDir);
// Shadow calculation
float shadow = calculateShadow(fragWorldPos);
lighting *= (1.0 - shadow * 0.7); // Soften shadows
// Combine lighting
vec3 finalColor = ambient + lighting;
// Fog
if (pc.enableFog != 0) {
float fogFactor = 1.0 - exp(-pc.fogDensity * length(pc.cameraPos - fragWorldPos));
finalColor = mix(finalColor, pc.fogColor, fogFactor);
}
// Simple tone mapping (Reinhard)
finalColor = finalColor / (finalColor + vec3(1.0));
// Gamma correction
finalColor = pow(finalColor, vec3(1.0 / pc.gamma));
outColor = vec4(finalColor, 1.0);
}
]], material_albedo, material_roughness, material_metallic, light_color, light_intensity)
end
local function build_vertex_color_sources()
return {
vertex = vertex_color_source,
fragment = vertex_color_fragment_source,
}
end
local function build_gui_2d_sources()
return {
vertex = gui_2d_vertex_source,
fragment = gui_2d_fragment_source,
}
end
local function build_solid_color_sources(options)
local color = normalize_color(options.color)
local fragment = string.format([[
#version 450
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(%.3f, %.3f, %.3f, %.3f);
}
]], color[1], color[2], color[3], color[4])
return {
vertex = vertex_color_source,
fragment = fragment,
}
end
local function build_shadow_sources()
return {
vertex = shadow_vertex_source,
fragment = shadow_fragment_source,
}
end
local function build_cube_rainbow_sources(options)
return {
vertex = vertex_world_color_source,
fragment = build_cube_rainbow_fragment_source(options),
}
end
local function build_solid_lit_sources(options)
return {
vertex = vertex_world_color_source,
fragment = build_solid_lit_fragment_source(options),
}
end
local function build_floor_sources(options)
return {
vertex = vertex_world_color_source,
fragment = build_floor_fragment_source(options),
}
end
local function build_wall_sources(options)
return {
vertex = vertex_world_color_source,
fragment = build_wall_fragment_source(options),
}
end
local function build_ceiling_sources(options)
return {
vertex = vertex_world_color_source,
fragment = build_ceiling_fragment_source(options),
}
end
local function build_pbr_sources(options)
return {
vertex = pbr_vertex_source,
fragment = build_pbr_fragment_source(options),
}
end
local function build_ssgi_sources()
return {
vertex = fullscreen_vertex_source,
fragment = ssgi_fragment_source,
}
end
local function build_volumetric_sources()
return {
vertex = fullscreen_vertex_source,
fragment = volumetric_fragment_source,
}
end
local templates = {}
local function register_template_object(template)
if type(template) ~= "table" or type(template.name) ~= "string" or template.name == "" then
error("Shader template must include a non-empty name")
end
if type(template.Generate) ~= "function" then
error("Shader template must implement Generate(options)")
end
templates[template.name] = template
end
register_template_object(ShaderTemplate:new("vertex_color", build_vertex_color_sources))
register_template_object(ShaderTemplate:new("gui_2d", build_gui_2d_sources))
register_template_object(ShaderTemplate:new("solid_color", build_solid_color_sources, {
color = {1.0, 1.0, 1.0, 1.0},
}))
register_template_object(ShaderTemplate:new("shadow", build_shadow_sources))
register_template_object(ShaderTemplate:new("cube_rainbow", build_cube_rainbow_sources, {
band_scale = 0.35,
diagonal_scale = 0.25,
mix_factor = 0.08,
ambient_strength = 0.28,
key_intensity = 0.9,
fill_intensity = 0.4,
key_dir = {-0.35, 1.0, -0.25},
fill_dir = {0.45, 0.6, 0.2},
}))
register_template_object(ShaderTemplate:new("solid_lit", build_solid_lit_sources, {
light_color = {1.0, 0.9, 0.6},
light_intensity = 0.9,
ambient_strength = 0.22,
}))
register_template_object(ShaderTemplate:new("room_floor", build_floor_sources, {
surface_base = {0.02, 0.95, 0.72},
checker_scale = 0.55,
light_color = {1.0, 0.92, 0.7},
light_intensity = 1.0,
ambient_strength = 0.3,
}))
register_template_object(ShaderTemplate:new("room_wall", build_wall_sources, {
surface_base = {0.98, 0.18, 0.08},
plank_scale = 0.4,
light_color = {1.0, 0.88, 0.6},
light_intensity = 0.95,
ambient_strength = 0.24,
}))
register_template_object(ShaderTemplate:new("room_ceiling", build_ceiling_sources, {
surface_base = {0.98, 0.96, 0.1},
grid_scale = 0.45,
light_color = {0.92, 0.96, 1.0},
light_intensity = 0.85,
ambient_strength = 0.38,
}))
register_template_object(ShaderTemplate:new("pbr", build_pbr_sources, {
material_albedo = {0.8, 0.8, 0.8},
material_roughness = 0.3,
material_metallic = 0.1,
light_color = {1.0, 0.9, 0.6},
light_intensity = 1.2,
}))
register_template_object(ShaderTemplate:new("ssgi", build_ssgi_sources))
register_template_object(ShaderTemplate:new("volumetric", build_volumetric_sources))
shader_toolkit.templates = templates
shader_toolkit.ShaderTemplate = ShaderTemplate
shader_toolkit.ShaderVariant = ShaderVariant
local function is_template_object(value)
return type(value) == "table"
and type(value.name) == "string"
and value.name ~= ""
and type(value.Generate) == "function"
end
local function resolve_template(template_ref)
if type(template_ref) == "string" then
return templates[template_ref]
end
if is_template_object(template_ref) then
return template_ref
end
return nil
end
local function resolve_variant(value)
if getmetatable(value) == ShaderVariant then
return value
end
return ShaderVariant:new(value)
end
function shader_toolkit.create_template(name, generator, defaults)
return ShaderTemplate:new(name, generator, defaults)
end
function shader_toolkit.create_variant(options)
return ShaderVariant:new(options)
end
function shader_toolkit.register_template(name_or_template, generator, defaults)
local template = name_or_template
if type(name_or_template) == "string" then
template = ShaderTemplate:new(name_or_template, generator, defaults)
end
if not is_template_object(template) then
error("Template registration requires a ShaderTemplate or name/generator pair")
end
register_template_object(template)
end
function shader_toolkit.list_templates()
local names = {}
for name in pairs(templates) do
names[#names + 1] = name
end
table.sort(names)
return names
end
function shader_toolkit.generate_variant(options)
local variant = resolve_variant(options)
local template = resolve_template(variant.template)
if not template then
error("Unknown shader template: " .. tostring(variant.template))
end
local output_mode = variant.output_mode or "source"
local output_name = normalize_output_name(variant.output_name or variant.key or template.name)
if not output_name then
error("Shader variant requires output_name or key")
end
local sources = template:Generate(variant.parameters)
if type(sources) ~= "table" or not sources.vertex or not sources.fragment then
error("Shader template did not return vertex and fragment source")
end
if output_mode ~= "files" then
if variant.compile == true then
error("Inline shader output does not support external compilation")
end
local result = {}
for stage, source in pairs(sources) do
if type(source) == "string" then
result[stage .. "_source"] = source
end
end
return result
end
local output_dir = resolve_output_dir(variant.output_dir)
if not ensure_directory(output_dir) then
error("Failed to create shader output directory: " .. output_dir)
end
local base_name = path_join(output_dir, output_name)
local vertex_source = base_name .. ".vert"
local fragment_source = base_name .. ".frag"
local vertex_spv = vertex_source .. ".spv"
local fragment_spv = fragment_source .. ".spv"
write_text_file_if_changed(vertex_source, sources.vertex)
write_text_file_if_changed(fragment_source, sources.fragment)
local compile = variant.compile ~= false
if compile then
local compiler = variant.compiler or detect_compiler()
if not compiler then
error("No shader compiler found. Install glslangValidator or glslc, or pass options.compiler")
end
local skip_if_present = variant.skip_if_present == true
if not (skip_if_present and file_exists(vertex_spv)) then
compile_shader(compiler, vertex_source, vertex_spv)
end
if not (skip_if_present and file_exists(fragment_spv)) then
compile_shader(compiler, fragment_source, fragment_spv)
end
end
if compile then
return {vertex = vertex_spv, fragment = fragment_spv}
end
return {vertex = vertex_source, fragment = fragment_source}
end
function shader_toolkit.generate_variants(variant_list)
if type(variant_list) ~= "table" then
error("Shader variants must be a list of tables")
end
local result = {}
for index, entry in ipairs(variant_list) do
if type(entry) ~= "table" then
error("Shader variant at index " .. index .. " must be a table")
end
local variant = resolve_variant(entry)
local key = variant.key
if type(key) ~= "string" or key == "" then
error("Shader variant at index " .. index .. " must include a non-empty key")
end
result[key] = shader_toolkit.generate_variant(variant)
end
return result
end
function shader_toolkit.generate_cube_demo_variants(options)
local settings = options or {}
local parameters = settings.parameters or {}
local output_mode = settings.output_mode or "source"
local compile = settings.compile
if compile == nil then
compile = false
end
local output_dir = settings.output_dir
local variants = {
shader_toolkit.create_variant({
key = "default",
template = "cube_rainbow",
output_name = "cube",
output_mode = output_mode,
compile = compile,
output_dir = output_dir,
parameters = parameters.default,
}),
shader_toolkit.create_variant({
key = "solid",
template = "solid_lit",
output_mode = output_mode,
compile = compile,
output_dir = output_dir,
parameters = parameters.solid,
}),
shader_toolkit.create_variant({
key = "floor",
template = "room_floor",
output_mode = output_mode,
compile = compile,
output_dir = output_dir,
parameters = parameters.floor,
}),
shader_toolkit.create_variant({
key = "wall",
template = "room_wall",
output_mode = output_mode,
compile = compile,
output_dir = output_dir,
parameters = parameters.wall,
}),
shader_toolkit.create_variant({
key = "ceiling",
template = "room_ceiling",
output_mode = output_mode,
compile = compile,
output_dir = output_dir,
parameters = parameters.ceiling,
}),
shader_toolkit.create_variant({
key = "pbr",
template = "pbr",
output_mode = output_mode,
compile = compile,
output_dir = output_dir,
parameters = parameters.pbr,
}),
}
return shader_toolkit.generate_variants(variants)
end
return shader_toolkit