mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
feat: Add shaderc dependency and implement shader toolkit script
This commit is contained in:
@@ -38,6 +38,7 @@ class SDL3CppConan(ConanFile):
|
||||
self.requires("vulkan-memory-allocator/3.3.0")
|
||||
self.requires("spirv-tools/1.4.313.0")
|
||||
self.requires("spirv-headers/1.4.313.0")
|
||||
self.requires("shaderc/2025.3")
|
||||
self.requires("cpptrace/1.0.4")
|
||||
self.requires("ogg/1.3.5")
|
||||
self.requires("theora/1.1.1")
|
||||
|
||||
359
scripts/shader_toolkit.lua
Normal file
359
scripts/shader_toolkit.lua
Normal file
@@ -0,0 +1,359 @@
|
||||
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 normalize_color(color)
|
||||
if type(color) ~= "table" then
|
||||
return {1.0, 1.0, 1.0, 1.0}
|
||||
end
|
||||
local r = tonumber(color[1]) or 1.0
|
||||
local g = tonumber(color[2]) or 1.0
|
||||
local b = tonumber(color[3]) or 1.0
|
||||
local a = tonumber(color[4]) or 1.0
|
||||
return {r, g, b, a}
|
||||
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 templates = {
|
||||
vertex_color = function()
|
||||
return {
|
||||
vertex = vertex_color_source,
|
||||
fragment = vertex_color_fragment_source,
|
||||
}
|
||||
end,
|
||||
solid_color = function(options)
|
||||
local color = normalize_color(options and 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,
|
||||
}
|
||||
|
||||
shader_toolkit.templates = templates
|
||||
|
||||
function shader_toolkit.register_template(name, generator)
|
||||
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
|
||||
templates[name] = generator
|
||||
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)
|
||||
if type(options) ~= "table" then
|
||||
error("Shader variant options must be a table")
|
||||
end
|
||||
local template_name = options.template
|
||||
if type(template_name) ~= "string" or template_name == "" then
|
||||
error("Shader variant template must be a non-empty string")
|
||||
end
|
||||
local template = templates[template_name]
|
||||
if not template then
|
||||
error("Unknown shader template: " .. template_name)
|
||||
end
|
||||
local output_name = normalize_output_name(options.output_name or options.key or template_name)
|
||||
if not output_name then
|
||||
error("Shader variant requires output_name or key")
|
||||
end
|
||||
local output_dir = resolve_output_dir(options.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"
|
||||
|
||||
local sources = template(options)
|
||||
if type(sources) ~= "table" or not sources.vertex or not sources.fragment then
|
||||
error("Shader template did not return vertex and fragment source")
|
||||
end
|
||||
|
||||
write_text_file(vertex_source, sources.vertex)
|
||||
write_text_file(fragment_source, sources.fragment)
|
||||
|
||||
local compile = options.compile ~= false
|
||||
if compile then
|
||||
local compiler = options.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 = options.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, options in ipairs(variant_list) do
|
||||
if type(options) ~= "table" then
|
||||
error("Shader variant at index " .. index .. " must be a table")
|
||||
end
|
||||
local key = options.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(options)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return shader_toolkit
|
||||
Reference in New Issue
Block a user