From ac91a0a37913ee4ceffa3d6324a6769fa1f473af Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:24:35 -0500 Subject: [PATCH] Add in documentation --- .../examples/shader-demo/default-shader.lua | 260 ++++++++++++++++++ .../shader-demo/invert-color-shader.lua | 225 +++++++++++++++ .../examples/shader-demo/mirror-shader.lua | 42 +++ docs/lua/guides/shaders.md | 169 ++++++++++++ 4 files changed, 696 insertions(+) create mode 100644 docs/lua/examples/shader-demo/default-shader.lua create mode 100644 docs/lua/examples/shader-demo/invert-color-shader.lua create mode 100644 docs/lua/examples/shader-demo/mirror-shader.lua create mode 100644 docs/lua/guides/shaders.md diff --git a/docs/lua/examples/shader-demo/default-shader.lua b/docs/lua/examples/shader-demo/default-shader.lua new file mode 100644 index 000000000..34198df65 --- /dev/null +++ b/docs/lua/examples/shader-demo/default-shader.lua @@ -0,0 +1,260 @@ +---@param item number +---@param with_alpha boolean +---@param only_alpha boolean +---@param hint_single_element boolean +---@return string +local function shader_item_to_str(item, with_alpha, only_alpha, hint_single_element) + if not only_alpha then + if item == SHADER_0 then + return with_alpha and "vec4(0.0, 0.0, 0.0, 0.0)" or "vec3(0.0, 0.0, 0.0)" + elseif item == SHADER_1 then + return with_alpha and "vec4(1.0, 1.0, 1.0, 1.0)" or "vec3(1.0, 1.0, 1.0)" + elseif item >= SHADER_INPUT_1 and item <= SHADER_INPUT_8 then + local idx = item - SHADER_INPUT_1 + 1 + local name = "vInput" .. idx + return with_alpha and name or (name .. ".rgb") + elseif item == SHADER_TEXEL0 then + return with_alpha and "texVal0" or "texVal0.rgb" + elseif item == SHADER_TEXEL0A then + if hint_single_element then return "texVal0.a" end + return with_alpha and + "vec4(texVal0.a, texVal0.a, texVal0.a, texVal0.a)" or + "vec3(texVal0.a, texVal0.a, texVal0.a)" + elseif item == SHADER_TEXEL1 then + return with_alpha and "texVal1" or "texVal1.rgb" + elseif item == SHADER_TEXEL1A then + if hint_single_element then return "texVal1.a" end + return with_alpha and + "vec4(texVal1.a, texVal1.a, texVal1.a, texVal1.a)" or + "vec3(texVal1.a, texVal1.a, texVal1.a)" + elseif item == SHADER_COMBINED then + return with_alpha and "texel" or "texel.rgb" + elseif item == SHADER_COMBINEDA then + if hint_single_element then return "texel.a" end + return with_alpha and + "vec4(texel.a, texel.a, texel.a, texel.a)" or + "vec3(texel.a, texel.a, texel.a)" + elseif item == SHADER_NOISE then + return with_alpha and "vec4(noise)" or "vec3(noise)" + end + else + if item == SHADER_0 then + return "0.0" + elseif item == SHADER_1 then + return "1.0" + elseif item >= SHADER_INPUT_1 and item <= SHADER_INPUT_8 then + local idx = item - SHADER_INPUT_1 + 1 + return "vInput" .. idx .. ".a" + elseif item == SHADER_TEXEL0 or item == SHADER_TEXEL0A then + return "texVal0.a" + elseif item == SHADER_TEXEL1 or item == SHADER_TEXEL1A then + return "texVal1.a" + elseif item == SHADER_COMBINED or item == SHADER_COMBINEDA then + return "texel.a" + elseif item == SHADER_NOISE then + return "noise.a" + end + end + return "unknown" +end + +---@param cmd number[] +---@param do_single boolean +---@param do_multiply boolean +---@param do_mix boolean +---@param with_alpha boolean +---@param only_alpha boolean +---@return string +local function append_formula(cmd, do_single, do_multiply, do_mix, with_alpha, only_alpha) + local base = (only_alpha and 4 or 0) + + if do_single then + return shader_item_to_str(cmd[base + 4], with_alpha, only_alpha, false) + elseif do_multiply then + local a = shader_item_to_str(cmd[base + 1], with_alpha, only_alpha, false) + local c = shader_item_to_str(cmd[base + 3], with_alpha, only_alpha, true) + return a .. " * " .. c + elseif do_mix then + local b = shader_item_to_str(cmd[base + 2], with_alpha, only_alpha, false) + local a = shader_item_to_str(cmd[base + 1], with_alpha, only_alpha, false) + local c = shader_item_to_str(cmd[base + 3], with_alpha, only_alpha, true) + return "mix(" .. b .. ", " .. a .. ", " .. c .. ")" + else + local a = shader_item_to_str(cmd[base + 1], with_alpha, only_alpha, false) + local b = shader_item_to_str(cmd[base + 2], with_alpha, only_alpha, false) + local c = shader_item_to_str(cmd[base + 3], with_alpha, only_alpha, true) + local d = shader_item_to_str(cmd[base + 4], with_alpha, only_alpha, false) + return "(" .. a .. " - " .. b .. ") * " .. c .. " + " .. d + end +end + +---@param cc ColorCombiner +---@param shaderIndex integer +local function on_vertex_shader_create(cc, shaderIndex) + local vertexShader = {} + table.insert(vertexShader, "#version 150") + table.insert(vertexShader, "in vec4 aVtxPos;") + table.insert(vertexShader, "in vec2 aTexCoord;") + table.insert(vertexShader, "out vec2 vTexCoord;") + table.insert(vertexShader, "in vec4 aFog;") + table.insert(vertexShader, "out vec4 vFog;") + table.insert(vertexShader, "in vec2 aLightMap;") + table.insert(vertexShader, "out vec2 vLightMap;") + + for i = 1, CC_MAX_INPUTS do + table.insert(vertexShader, string.format("in vec4 aInput%d;", i)) + table.insert(vertexShader, string.format("out vec4 vInput%d;", i)) + end + + table.insert(vertexShader, "in vec3 aNormal;") + table.insert(vertexShader, "out vec3 vNormal;") + table.insert(vertexShader, "in vec3 aBarycentric;") + table.insert(vertexShader, "out vec3 vBarycentric;") + + table.insert(vertexShader, "void main() {") + table.insert(vertexShader, "vTexCoord = aTexCoord;") + table.insert(vertexShader, "vFog = aFog;") + table.insert(vertexShader, "vLightMap = aLightMap;") + for i = 1, CC_MAX_INPUTS do + table.insert(vertexShader, string.format("vInput%d = aInput%d;", i, i)) + end + table.insert(vertexShader, "vNormal = aNormal;") + table.insert(vertexShader, "vBarycentric = aBarycentric;") + table.insert(vertexShader, "gl_Position = aVtxPos;") + table.insert(vertexShader, "}") + + return table.concat(vertexShader, "\n") +end + +---@param cc ColorCombiner +---@param shaderIndex integer +local function on_fragment_shader_create(cc, shaderIndex) + local ccf = gfx_color_combiner_get_features(cc) + local opt_alpha = cc.cm.flags & USE_ALPHA ~= 0 + local opt_fog = cc.cm.flags & USE_FOG ~= 0 + local opt_texture_edge = cc.cm.flags & TEXTURE_EDGE ~= 0 + local opt_2cycle = cc.cm.flags & USE_2CYCLE ~= 0 + local opt_light_map = cc.cm.flags & LIGHT_MAP ~= 0 + local opt_dither = cc.cm.flags & USE_DITHER ~= 0 + + local fragmentShader = {} + table.insert(fragmentShader, "#version 150") + + table.insert(fragmentShader, "out vec4 fragColor;") + table.insert(fragmentShader, "in vec2 vTexCoord;") + table.insert(fragmentShader, "in vec4 vFog;") + table.insert(fragmentShader, "in vec2 vLightMap;") + + for i = 1, CC_MAX_INPUTS do + table.insert(fragmentShader, string.format("in vec4 vInput%d;", i)) + end + + table.insert(fragmentShader, "in vec3 vNormal;") + table.insert(fragmentShader, "in vec3 vBarycentric;") + + if ccf.used_textures[1] then + table.insert(fragmentShader, "uniform sampler2D uTex0;") + table.insert(fragmentShader, "uniform vec2 uTex0Size;") + table.insert(fragmentShader, "uniform bool uTex0Filter;") + end + if ccf.used_textures[2] then + table.insert(fragmentShader, "uniform sampler2D uTex1;") + table.insert(fragmentShader, "uniform vec2 uTex1Size;") + table.insert(fragmentShader, "uniform bool uTex1Filter;") + end + + if ccf.used_textures[1] or ccf.used_textures[2] then + table.insert(fragmentShader, "#define TEX_OFFSET(off) texture(tex, texCoord - (off)/texSize)") + table.insert(fragmentShader, "vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) {") + table.insert(fragmentShader, " vec2 offset = fract(texCoord*texSize - vec2(0.5));") + table.insert(fragmentShader, " offset -= step(1.0, offset.x + offset.y);") + table.insert(fragmentShader, " vec4 c0 = TEX_OFFSET(offset);") + table.insert(fragmentShader, " vec4 c1 = TEX_OFFSET(vec2(offset.x - sign(offset.x), offset.y));") + table.insert(fragmentShader, " vec4 c2 = TEX_OFFSET(vec2(offset.x, offset.y - sign(offset.y)));") + table.insert(fragmentShader, " return c0 + abs(offset.x)*(c1-c0) + abs(offset.y)*(c2-c0);") + table.insert(fragmentShader, "}") + table.insert(fragmentShader, "vec4 sampleTex(in sampler2D tex, in vec2 uv, in vec2 texSize, in bool dofilter, in int filterType) {") + table.insert(fragmentShader, " if (dofilter && filterType == 2) return filter3point(tex, uv, texSize);") + table.insert(fragmentShader, " else return texture(tex, uv);") + table.insert(fragmentShader, "}") + end + + if (opt_alpha and opt_dither) or ccf.do_noise then + table.insert(fragmentShader, "uniform float uFrameCount;") + table.insert(fragmentShader, "float random(in vec3 value) {") + table.insert(fragmentShader, " float random = dot(sin(value), vec3(12.9898, 78.233, 37.719));") + table.insert(fragmentShader, " return fract(sin(random) * 143758.5453);") + table.insert(fragmentShader, "}") + end + + if opt_light_map then + table.insert(fragmentShader, "uniform vec3 uLightmapColor;") + end + table.insert(fragmentShader, "uniform int uFilter;") + + table.insert(fragmentShader, "void main() {") + if (opt_alpha and opt_dither) or ccf.do_noise then + table.insert(fragmentShader, "float noise = floor(random(floor(vec3(gl_FragCoord.xy, uFrameCount))) + 0.5);") + end + + if ccf.used_textures[1] then + table.insert(fragmentShader, "vec4 texVal0 = sampleTex(uTex0, vTexCoord, uTex0Size, uTex0Filter, uFilter);") + end + if ccf.used_textures[2] then + if opt_light_map then + table.insert(fragmentShader, "vec4 texVal1 = sampleTex(uTex1, vLightMap, uTex1Size, uTex1Filter, uFilter);") + table.insert(fragmentShader, "texVal0.rgb *= uLightmapColor.rgb;") + table.insert(fragmentShader, "texVal1.rgb = texVal1.rgb * texVal1.rgb + texVal1.rgb;") + else + table.insert(fragmentShader, "vec4 texVal1 = sampleTex(uTex1, vTexCoord, uTex1Size, uTex1Filter, uFilter);") + end + end + + table.insert(fragmentShader, (opt_alpha and "vec4 texel = " or "vec3 texel = ")) + + for i = 0, (opt_2cycle and 1 or 0) do + local cmd = {} + for j = 0, CC_MAX_INPUTS - 1 do + cmd[j + 1] = cc.shader_commands[i * CC_MAX_INPUTS + j + 1] + end + local idx = i * 2 + + if not ccf.color_alpha_same[i + 1] and opt_alpha then + local color = append_formula(cmd, ccf.do_single[idx + 1], ccf.do_multiply[idx + 1], ccf.do_mix[idx + 1], false, false) + local alpha = append_formula(cmd, ccf.do_single[idx + 2], ccf.do_multiply[idx + 2], ccf.do_mix[idx + 2], true, true) + table.insert(fragmentShader, "vec4(" .. color .. ", " .. alpha .. ")") + else + table.insert(fragmentShader, append_formula(cmd, ccf.do_single[idx + 1], ccf.do_multiply[idx + 1], ccf.do_mix[idx + 1], opt_alpha, false)) + end + table.insert(fragmentShader, ";") + if i == 0 and opt_2cycle then table.insert(fragmentShader, "texel = ") end + end + + if opt_texture_edge and opt_alpha then + table.insert(fragmentShader, "if (texel.a > 0.3) texel.a = 1.0; else discard;") + end + if opt_fog then + if opt_alpha then + table.insert(fragmentShader, "texel = vec4(mix(texel.rgb, vFog.rgb, vFog.a), texel.a);") + else + table.insert(fragmentShader, "texel = mix(texel, vFog.rgb, vFog.a);") + end + end + if opt_alpha and opt_dither then + table.insert(fragmentShader, "texel.a *= noise;") + end + + if opt_alpha then + table.insert(fragmentShader, "fragColor = texel;") + else + table.insert(fragmentShader, "fragColor = vec4(texel, 1.0);") + end + table.insert(fragmentShader, "}") + + return table.concat(fragmentShader, "\n") +end + +hook_event(HOOK_ON_VERTEX_SHADER_CREATE, on_vertex_shader_create) +hook_event(HOOK_ON_FRAGMENT_SHADER_CREATE, on_fragment_shader_create) + +gfx_reload_shaders() \ No newline at end of file diff --git a/docs/lua/examples/shader-demo/invert-color-shader.lua b/docs/lua/examples/shader-demo/invert-color-shader.lua new file mode 100644 index 000000000..2c02e60c3 --- /dev/null +++ b/docs/lua/examples/shader-demo/invert-color-shader.lua @@ -0,0 +1,225 @@ +-- name: Inverted Color Shader +-- description: This is an example of a fragment shader! + +-- function taken from C +---@param item number +---@param with_alpha boolean +---@param only_alpha boolean +---@param hint_single_element boolean +---@return string +local function shader_item_to_str(item, with_alpha, only_alpha, hint_single_element) + if not only_alpha then + if item == SHADER_0 then + return with_alpha and "vec4(0.0, 0.0, 0.0, 0.0)" or "vec3(0.0, 0.0, 0.0)" + elseif item == SHADER_1 then + return with_alpha and "vec4(1.0, 1.0, 1.0, 1.0)" or "vec3(1.0, 1.0, 1.0)" + elseif item >= SHADER_INPUT_1 and item <= SHADER_INPUT_8 then + local idx = item - SHADER_INPUT_1 + 1 + local name = "vInput" .. idx + return with_alpha and name or (name .. ".rgb") + elseif item == SHADER_TEXEL0 then + return with_alpha and "texVal0" or "texVal0.rgb" + elseif item == SHADER_TEXEL0A then + if hint_single_element then return "texVal0.a" end + return with_alpha and + "vec4(texVal0.a, texVal0.a, texVal0.a, texVal0.a)" or + "vec3(texVal0.a, texVal0.a, texVal0.a)" + elseif item == SHADER_TEXEL1 then + return with_alpha and "texVal1" or "texVal1.rgb" + elseif item == SHADER_TEXEL1A then + if hint_single_element then return "texVal1.a" end + return with_alpha and + "vec4(texVal1.a, texVal1.a, texVal1.a, texVal1.a)" or + "vec3(texVal1.a, texVal1.a, texVal1.a)" + elseif item == SHADER_COMBINED then + return with_alpha and "texel" or "texel.rgb" + elseif item == SHADER_COMBINEDA then + if hint_single_element then return "texel.a" end + return with_alpha and + "vec4(texel.a, texel.a, texel.a, texel.a)" or + "vec3(texel.a, texel.a, texel.a)" + elseif item == SHADER_NOISE then + return with_alpha and "vec4(noise)" or "vec3(noise)" + end + else + if item == SHADER_0 then + return "0.0" + elseif item == SHADER_1 then + return "1.0" + elseif item >= SHADER_INPUT_1 and item <= SHADER_INPUT_8 then + local idx = item - SHADER_INPUT_1 + 1 + return "vInput" .. idx .. ".a" + elseif item == SHADER_TEXEL0 or item == SHADER_TEXEL0A then + return "texVal0.a" + elseif item == SHADER_TEXEL1 or item == SHADER_TEXEL1A then + return "texVal1.a" + elseif item == SHADER_COMBINED or item == SHADER_COMBINEDA then + return "texel.a" + elseif item == SHADER_NOISE then + return "noise.a" + end + end + return "unknown" +end + +-- function taken from C +---@param cmd number[] +---@param do_single boolean +---@param do_multiply boolean +---@param do_mix boolean +---@param with_alpha boolean +---@param only_alpha boolean +---@return string +local function append_formula(cmd, do_single, do_multiply, do_mix, with_alpha, only_alpha) + local base = (only_alpha and 4 or 0) + + local function item(idx, hint_single) + return shader_item_to_str(cmd[base + idx + 1], with_alpha, only_alpha, hint_single) + end + + if do_single then + return item(3, false) + elseif do_multiply then + return item(0, false) .. " * " .. item(2, true) + elseif do_mix then + return "mix(" .. item(1, false) .. ", " .. item(0, false) .. ", " .. item(2, true) .. ")" + else + return "(" .. item(0, false) .. " - " .. item(1, false) .. ") * " .. item(2, true) .. " + " .. item(3, false) + end +end + +---@param cc ColorCombiner +---@param shaderIndex integer +local function on_fragment_shader_create(cc, shaderIndex) + -- egt color combiner info and features + local ccf = gfx_color_combiner_get_features(cc) + local opt_alpha = cc.cm.flags & USE_ALPHA ~= 0 + local opt_fog = cc.cm.flags & USE_FOG ~= 0 + local opt_texture_edge = cc.cm.flags & TEXTURE_EDGE ~= 0 + local opt_2cycle = cc.cm.flags & USE_2CYCLE ~= 0 + local opt_light_map = cc.cm.flags & LIGHT_MAP ~= 0 + local opt_dither = cc.cm.flags & USE_DITHER ~= 0 + + -- define a table that will later be turned into a string + local fragmentShader = {} + table.insert(fragmentShader, "#version 150") + + table.insert(fragmentShader, "out vec4 fragColor;") + table.insert(fragmentShader, "in vec2 vTexCoord;") + table.insert(fragmentShader, "in vec4 vFog;") + table.insert(fragmentShader, "in vec2 vLightMap;") + + for i = 1, CC_MAX_INPUTS do + table.insert(fragmentShader, string.format("in vec4 vInput%d;", i)) + end + + table.insert(fragmentShader, "in vec3 vNormal;") + table.insert(fragmentShader, "in vec3 vBarycentric;") + + if ccf.used_textures[1] then + table.insert(fragmentShader, "uniform sampler2D uTex0;") + table.insert(fragmentShader, "uniform vec2 uTex0Size;") + table.insert(fragmentShader, "uniform bool uTex0Filter;") + end + if ccf.used_textures[2] then + table.insert(fragmentShader, "uniform sampler2D uTex1;") + table.insert(fragmentShader, "uniform vec2 uTex1Size;") + table.insert(fragmentShader, "uniform bool uTex1Filter;") + end + + if ccf.used_textures[1] or ccf.used_textures[2] then + table.insert(fragmentShader, "#define TEX_OFFSET(off) texture(tex, texCoord - (off)/texSize)") + table.insert(fragmentShader, "vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) {") + table.insert(fragmentShader, " vec2 offset = fract(texCoord*texSize - vec2(0.5));") + table.insert(fragmentShader, " offset -= step(1.0, offset.x + offset.y);") + table.insert(fragmentShader, " vec4 c0 = TEX_OFFSET(offset);") + table.insert(fragmentShader, " vec4 c1 = TEX_OFFSET(vec2(offset.x - sign(offset.x), offset.y));") + table.insert(fragmentShader, " vec4 c2 = TEX_OFFSET(vec2(offset.x, offset.y - sign(offset.y)));") + table.insert(fragmentShader, " return c0 + abs(offset.x)*(c1-c0) + abs(offset.y)*(c2-c0);") + table.insert(fragmentShader, "}") + table.insert(fragmentShader, "vec4 sampleTex(in sampler2D tex, in vec2 uv, in vec2 texSize, in bool dofilter, in int filterType) {") + table.insert(fragmentShader, " if (dofilter && filterType == 2) return filter3point(tex, uv, texSize);") + table.insert(fragmentShader, " else return texture(tex, uv);") + table.insert(fragmentShader, "}") + end + + if (opt_alpha and opt_dither) or ccf.do_noise then + table.insert(fragmentShader, "uniform float uFrameCount;") + table.insert(fragmentShader, "float random(in vec3 value) {") + table.insert(fragmentShader, " float random = dot(sin(value), vec3(12.9898, 78.233, 37.719));") + table.insert(fragmentShader, " return fract(sin(random) * 143758.5453);") + table.insert(fragmentShader, "}") + end + + if opt_light_map then + table.insert(fragmentShader, "uniform vec3 uLightmapColor;") + end + table.insert(fragmentShader, "uniform int uFilter;") + + table.insert(fragmentShader, "void main() {") + if (opt_alpha and opt_dither) or ccf.do_noise then + table.insert(fragmentShader, "float noise = floor(random(floor(vec3(gl_FragCoord.xy, uFrameCount))) + 0.5);") + end + + if ccf.used_textures[1] then + table.insert(fragmentShader, "vec4 texVal0 = sampleTex(uTex0, vTexCoord, uTex0Size, uTex0Filter, uFilter);") + end + if ccf.used_textures[2] then + if opt_light_map then + table.insert(fragmentShader, "vec4 texVal1 = sampleTex(uTex1, vLightMap, uTex1Size, uTex1Filter, uFilter);") + table.insert(fragmentShader, "texVal0.rgb *= uLightmapColor.rgb;") + table.insert(fragmentShader, "texVal1.rgb = texVal1.rgb * texVal1.rgb + texVal1.rgb;") + else + table.insert(fragmentShader, "vec4 texVal1 = sampleTex(uTex1, vTexCoord, uTex1Size, uTex1Filter, uFilter);") + end + end + + table.insert(fragmentShader, (opt_alpha and "vec4 texel = " or "vec3 texel = ")) + + for i = 0, (opt_2cycle and 1 or 0) do + local cmd = {} + for j = 0, CC_MAX_INPUTS - 1 do + cmd[j + 1] = cc.shader_commands[i * CC_MAX_INPUTS + j + 1] + end + local idx = i * 2 + + if not ccf.color_alpha_same[i + 1] and opt_alpha then + local color = append_formula(cmd, ccf.do_single[idx + 1], ccf.do_multiply[idx + 1], ccf.do_mix[idx + 1], false, false) + local alpha = append_formula(cmd, ccf.do_single[idx + 2], ccf.do_multiply[idx + 2], ccf.do_mix[idx + 2], true, true) + table.insert(fragmentShader, "vec4(" .. color .. ", " .. alpha .. ")") + else + table.insert(fragmentShader, append_formula(cmd, ccf.do_single[idx + 1], ccf.do_multiply[idx + 1], ccf.do_mix[idx + 1], opt_alpha, false)) + end + table.insert(fragmentShader, ";") + if i == 0 and opt_2cycle then table.insert(fragmentShader, "texel = ") end + end + + if opt_texture_edge and opt_alpha then + table.insert(fragmentShader, "if (texel.a > 0.3) texel.a = 1.0; else discard;") + end + if opt_fog then + if opt_alpha then + table.insert(fragmentShader, "texel = vec4(mix(texel.rgb, vFog.rgb, vFog.a), texel.a);") + else + table.insert(fragmentShader, "texel = mix(texel, vFog.rgb, vFog.a);") + end + end + if opt_alpha and opt_dither then + table.insert(fragmentShader, "texel.a *= noise;") + end + + -- here we invert the texel color + if opt_alpha then + table.insert(fragmentShader, "fragColor = vec4(1.0 - texel.rgb, texel.a);") + else + table.insert(fragmentShader, "fragColor = vec4(1.0 - texel, 1.0);") + end + table.insert(fragmentShader, "}") + + return table.concat(fragmentShader, "\n") +end + +hook_event(HOOK_ON_FRAGMENT_SHADER_CREATE, on_fragment_shader_create) + +-- reload shaders so we can see our new shaders +gfx_reload_shaders() \ No newline at end of file diff --git a/docs/lua/examples/shader-demo/mirror-shader.lua b/docs/lua/examples/shader-demo/mirror-shader.lua new file mode 100644 index 000000000..af7d82450 --- /dev/null +++ b/docs/lua/examples/shader-demo/mirror-shader.lua @@ -0,0 +1,42 @@ + +---@param cc ColorCombiner +---@param shaderIndex integer +local function on_vertex_shader_create(cc, shaderIndex) + local vertexShader = {} + table.insert(vertexShader, "#version 150") + table.insert(vertexShader, "in vec4 aVtxPos;") + table.insert(vertexShader, "in vec2 aTexCoord;") + table.insert(vertexShader, "out vec2 vTexCoord;") + table.insert(vertexShader, "in vec4 aFog;") + table.insert(vertexShader, "out vec4 vFog;") + table.insert(vertexShader, "in vec2 aLightMap;") + table.insert(vertexShader, "out vec2 vLightMap;") + + for i = 1, CC_MAX_INPUTS do + table.insert(vertexShader, string.format("in vec4 aInput%d;", i)) + table.insert(vertexShader, string.format("out vec4 vInput%d;", i)) + end + + table.insert(vertexShader, "in vec3 aNormal;") + table.insert(vertexShader, "out vec3 vNormal;") + table.insert(vertexShader, "in vec3 aBarycentric;") + table.insert(vertexShader, "out vec3 vBarycentric;") + + table.insert(vertexShader, "void main() {") + table.insert(vertexShader, "vTexCoord = aTexCoord;") + table.insert(vertexShader, "vFog = aFog;") + table.insert(vertexShader, "vLightMap = aLightMap;") + for i = 1, CC_MAX_INPUTS do + table.insert(vertexShader, string.format("vInput%d = aInput%d;", i, i)) + end + table.insert(vertexShader, "vNormal = aNormal;") + table.insert(vertexShader, "vBarycentric = aBarycentric;") + table.insert(vertexShader, "gl_Position = vec4(aVtxPos.x * -1.0, aVtxPos.yzw);") + table.insert(vertexShader, "}") + + return table.concat(vertexShader, "\n") +end + +hook_event(HOOK_ON_VERTEX_SHADER_CREATE, on_vertex_shader_create) + +gfx_reload_shaders() \ No newline at end of file diff --git a/docs/lua/guides/shaders.md b/docs/lua/guides/shaders.md new file mode 100644 index 000000000..822eaaf4b --- /dev/null +++ b/docs/lua/guides/shaders.md @@ -0,0 +1,169 @@ +# Shaders + +## Resources + +- [Default C Shader in Lua](../examples/shader-demo/default-shader.lua) +- [Example Vertex Shader](../examples/shader-demo/mirror-shader.lua) +- [Example Fragment Shader](../examples/shader-demo/invert-color-shader.lua) + +## Before Starting... + +While this guide does go into a lot of detail, you should still have some basic knowledge on how writing mods in Lua works, and a bit of knowledge on how shaders work will also go a very long way. + +When testing shaders, it is recommended to launch your game with the terminal. While technically the game will fallback on default shaders if a shader fails to compile, there are multiple cases that are common enough to where you will appreciate running the game via a terminal since the console will be unavailable/invisible. + +## Creating Shaders + +Shaders can be created via the `HOOK_ON_VERTEX_SHADER_CREATE` and `HOOK_ON_FRAGMENT_SHADER_CREATE` hooks. These hooks provides a [ColorCombiner](../structs.md#ColorCombiner) and shader index, and expects you to return a vertex shader back. The vertex shader allows for manipulating vertex data, whereas the fragment shader allows for manipulating color data. You do not have to provide both, but if you only use one, you are expected to send/receive the proper data back. + +## Vertex Shaders + +The bread and butter of vertex shaders are inputs. Inputs are given by the game's code and can be used for getting the vertex data. A list of inputs can be found [here](#Inputs). Something important about inputs is that a fragment shader cannot get inputs from C, so you need to pass inputs from the vertex shader as outputs for the fragment shader. An example of that would be the texture coords. + +```lua +in vec2 aTexCoord; +out vec2 vTexCoord; +vTexCoord = aTexCoord; +``` + +As you can see, the input is gotten in the vertex shader, and the output is created, which will be received in the fragment shader. + +```lua +in vec2 vTexCoord; +``` + +Here, the fragment shader takes the tex coord output we created in the vertex shader. + +Getting back on track, for our vertex shader example, we are going to mirror the entire world. First, grab the [default C shader in Lua](../examples/shader-demo/default-shader.lua), specifically the vertex portion. Most of this shader will be passing inputs to the fragment shader, but what we care about is this portion: + +```lua +gl_Position = aVtxPos; +``` + +This sets the vertex position in opengl to our vertex position provided by C. There are 4 main components to this position: + +- `x` being the X coordinate +- `y` being the Y coordinate +- `z` being the Z coordinate +- `w` being the depth coordinate + +Importantly, this position is not in world space, it is instead in clip space, but that does not matter for our example. + +To get our mirror effect, we need to flip the `x` coordinate. So we need to create a vector 4 that flips our `x` component, and passes back our `y`, `z`, and `w` components as-is. This can be done via this syntax: + +```lua +gl_Position = vec4(aVtxPos.x * -1.0, aVtxPos.yzw); +``` + +If you run the mod, you should now have a mirrored game! That's the basic rundown on vertex shaders! If something still isn't working, compare your code with [the example vertex shader](../examples/shader-demo/mirror-shader.lua) and try to figure out what you did wrong. + +## Fragment Shaders + +Fragment shaders are quite a bit more involved, but I'll try to keep it simple. First, grab the [default C shader in Lua](../examples/shader-demo/default-shader.lua), specifically the fragment shader portion, as well as the `shader_item_to_str` and `append_formula` functions. While we won't be explaining what those do right now, to summarize, they are there to handle the color combiner. + +For our example fragment shader, we are going to be inverting the colors. Actually doing this is quite simple, but before we get to that, let's explain what we are working with: + +First, unlike the vertex shader, you have to define the output manually in a fragment shader. + +```lua +out vec4 fragColor; +``` + +In the fragment shader, we create an output for the fragment color. Unlike the vertex shader, an output does not go to another shader, instead, it goes to OpenGL to be used for color data. + +All the outputs in the vertex shader can be read in the fragment shader as inputs. This allows data to be carried over from one to the other. + +For our example, inverting the colors, we only have to look at 2 lines. One for if the color combiner specifies an alpha, and the other if it does not. If we do have an alpha, we want to preserve it when inverting the color. Colors in OpenGL, unlike the rest of sm64, are stored in between `0.0` and `1.0`. That means if I want to represent half of red, I would need to use `0.5` instead of `128`. For inverting a color this is simple. Combining these things, we need to subtract a full color, or `1.0`, by the rgb of `texel`, then pass in the alpha value as normal. + +```lua +fragColor = vec4(1.0 - texel.rgb, texel.a); +``` + +If the color combiner does not have an alpha value, then the alpha should simply be `1.0`, or fully opaque. + +```lua +fragColor = vec4(1.0 - texel.rgb, 1.0); +``` + +And that's it! All colors in your game should now be completely inverted! That's the basic rundown on fragment shaders! If something still isn't working, compare your code with [the example fragment shader](../examples/shader-demo/invert-color-shader.lua) and try to figure out what you did wrong. + +## Uniforms + +A shader may contain uniforms. As a naming convention, uniform variables typically start with a `u`, for instance, `uFrameCount`. There are multiple different uniforms already updated by C, but you can also update custom uniforms in Lua. First, here is a list of all the uniforms updated in C. + +| Uniform Name | Type | Description | +| ---- | ---- | ---- | +| `uTex0` | `sampler2D` | The primary texture | +| `uTex1` | `sampler2D` | The secondary texture | +| `uTex0Size` | `vec2` | The width and height of the primary texture | +| `uTex1Size` | `vec2` | The width and height of the secondary texture | +| `uTex0Filter` | `bool` | True if the first texture is using linear filtering | +| `uTex1Filter` | `bool` | True if the second texture is using linear filtering | +| `uFilter` | `int` | The current global filtering mode (0 = Point, 1 = Linear, 2 = Three-point) | +| `uFrameCount` | `float` | A timer that increases every frame | +| `uLightmapColor` | `vec3` | The RGB color multiplier applied to the environment/lightmap | +| `uModelViewMatrix` | `mat4` | Transforms objects from world space to view space | +| `uProjectionMatrix` | `mat4` | Transforms view space to clip space | +| `uInverseViewMatrix` | `mat4` | The inverse of the view matrix | + +For defining a custom uniform in lua, first, define the uniform and use the uniform as you intend in your shader code. Next you're going to want to store the shader index given by the hook. Create a table and store all your shader indexes. Then in lua, in any hook as seen fit, iterate through the list of shader indexes. Now, create a variable and set it to the returned value of `gfx_get_program_id_from_shader_index`. First, use that program with `gfx_use_program`, then get the uniform with `gfx_shader_get_uniform_location`. You should then set it with the appropriate `gfx_shader_set_` function. Here is an example: + +```lua +local fogColor = { r = 168, g = 175, b = 195 } +-- iterate through the shader indexes +for _, index in pairs(sShaderIndexes) do + -- get the program from the shader index + local program = gfx_get_program_id_from_shader_index(index) + -- use the program so we can set the uniforms + gfx_use_program(program) + + -- get the uniform, which is a color of type vec4 + local uFogColor = gfx_shader_get_uniform_location(program, "uFogColor") + -- set the vector 4 at that uniform location + gfx_shader_set_vec4(uFogColor, fogColor.r / 255, fogColor.g / 255, fogColor.b / 255, 1.0) + + local uFogIntensity = gfx_shader_get_uniform_location(program, "uFogIntensity") + gfx_shader_set_float(uFogIntensity, 5000.0) +end +``` + +That allows you to define your own uniforms and set your uniforms in Lua! + +Lastly, if you have multiple shaders, you should cleanup the list of shader indexes on a shader refresh. Use the `HOOK_ON_REFRESH_SHADERS` hook to reset the shader indexes table, or do anything you need to when it comes to refreshing shaders. + +```lua +hook_event(HOOK_ON_REFRESH_SHADERS, function () + sShaderIndexes = {} +end) +``` + +## Inputs + +Inputs are passed into the vertex shader for further use. Here is a list of inputs provided by C. + +| Input Name | Type | Description | +| ---- | ---- | ---- | +| `aVtxPos` | `vec4` | The vertex position (x, y, z, w) | +| `aTexCoord` | `vec2` | The standard UV mapping for the primary texture | +| `aNormal` | `vec3` | The direction the surface is facing | +| `aFog` | `vec4` | Fog data provided by C | +| `aLightMap` | `vec2` | UV coordinates for a light map | +| `aInputX` | `vec4` | Color/alpha input from the Color Combiner, with X being the input number | +| `aBarycentric` | `vec3` | The barycentric coordinates of the vertex within its triangle | + +## Dealing with the HUD and Skybox + +If you want to hide the HUD, you can by checking the vertex position. If the vertex position's z is greater than zero, it is not a hud element. By default the vertex position is not included in the fragment shader, so you may need to create a varying variable and send it over. + +To check for the skybox, the skybox lives somewhere in between layer 0 and layer 2. You should do a range check to find it, for instance, `aVtxPos.z > 0 && aVtxPos.z < 2`. + +Currently where the hud lives below 0 is a bit random, so right now it's best to either hide it all or none of it. + +## Limitations + +- No more than a single shader can be used at a time. This means that if 2 mods want to use their own shader, only one will be picked. +- Currently you can only do one shader pass. While for most cases a single shader pass works fine, for many types of shaders it is very limiting. This is a unfortunate limitation with the current system. +- There are only so many inputs. While I made sure to include as many as possible, there may still be some missing for your own shaders. +- DirectX and OpenGL Legacy support is non-existent. + +While these limitations may improve in the future, this is where we are stuck for right now. \ No newline at end of file