mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-25 14:15:02 +00:00
1609 lines
46 KiB
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
|