]> gitweb.ps.run Git - sporegirl/commitdiff
changezzz main
authorpatrick-scho <patrick.schoenberger@posteo.de>
Mon, 1 Sep 2025 13:07:41 +0000 (15:07 +0200)
committerpatrick-scho <patrick.schoenberger@posteo.de>
Mon, 1 Sep 2025 13:07:41 +0000 (15:07 +0200)
build.zig
build.zig.zon
shd/main.glsl [new file with mode: 0644]
shd/quad.glsl [deleted file]
src/main.zig

index 0fe3fbf063b9dc62f2e2fbfbc7fe5121ac0f045c..8742838255ecc52a099136961d51f2d0ee64115a 100644 (file)
--- a/build.zig
+++ b/build.zig
@@ -5,6 +5,7 @@ const ResolvedTarget = Build.ResolvedTarget;
 const Dependency = Build.Dependency;
 const sokol = @import("sokol");
 const cimgui = @import("cimgui");
+const shdc = @import("shdc");
 
 pub fn build(b: *Build) !void {
     const target = b.standardTargetOptions(.{});
@@ -30,11 +31,11 @@ pub fn build(b: *Build) !void {
 
     // shaders
     dep_sokol.artifact("sokol_clib").addIncludePath(b.path("ext/cimgui"));
-    const dep_shdc = dep_sokol.builder.dependency("shdc", .{});
-    const shdc_step = try sokol.shdc.createSourceFile(b, .{
+    const dep_shdc = b.dependency("shdc", .{});
+    const shdc_step = try shdc.createSourceFile(b, .{
         .shdc_dep = dep_shdc,
-        .input = "shd/quad.glsl",
-        .output = "src/shd/quad.glsl.zig",
+        .input = "shd/main.glsl",
+        .output = "src/shd/main.glsl.zig",
         .slang = .{ .glsl430 = true },
     });
 
@@ -64,16 +65,27 @@ pub fn build(b: *Build) !void {
     } else {
         const exe = try buildNative(b, mod_main);
         exe.step.dependOn(shdc_step);
+
+        exe.root_module.link_libc = true;
+        exe.root_module.linkSystemLibrary("X11", .{ .needed = true });
+        exe.root_module.linkSystemLibrary("Xcursor", .{ .needed = true });
+
+        const exe_check = b.addExecutable(.{
+            .name = "sporegirl",
+            .root_module = mod_main,
+        });
+        const check = b.step("check", "Check");
+        check.dependOn(&exe_check.step);
     }
 }
 
 fn buildNative(b: *Build, mod: *Build.Module) !*Build.Step.Compile {
     const exe = b.addExecutable(.{
-        .name = "demo",
+        .name = "sporegirl",
         .root_module = mod,
     });
     b.installArtifact(exe);
-    b.step("run", "Run demo").dependOn(&b.addRunArtifact(exe).step);
+    b.step("run", "Run Sporegirl").dependOn(&b.addRunArtifact(exe).step);
     return exe;
 }
 
index bc7ef945d718cd987f7ebe2d412b029994e33d90..74704ce3401827a303953493121598ed5b7f22d0 100644 (file)
@@ -9,12 +9,16 @@
 
     .dependencies = .{
         .sokol = .{
-            .url = "git+https://github.com/floooh/sokol-zig.git#zig-0.14.1",
-            .hash = "sokol-0.1.0-pb1HK26VLQC1XOkHDW-5TglgwypAKIHGR2HPOTP6limn",
+            .url = "git+https://github.com/floooh/sokol-zig.git#0831d1f15d2e1bc2d26a4556dba15b8962c29f00",
+            .hash = "sokol-0.1.0-pb1HKwHSLQAjEIRTGaVbHdtf6zt9VhNsOHuoVfTc2Z1i",
         },
         .cimgui = .{
-            .url = "git+https://github.com/floooh/dcimgui#3b98e0a57fc17cc72fdda6934bd932426778a16e",
-            .hash = "cimgui-0.1.0-44ClkczdkgCE9Z_0ehliUmEJjg-bjQVy1r55RQPw9N10",
+            .url = "git+https://github.com/floooh/dcimgui#de39f1d7106d448909d9776eb0049fbfed24d056",
+            .hash = "cimgui-0.1.0-44ClkQ_ekgDNAf1N4Fl1mJnVDyO83xbcqQElNUtURXcA",
+        },
+        .shdc = .{
+            .url = "git+https://github.com/floooh/sokol-tools-bin#d1a6a719f24852f104a7f5481beb31e42ebf5f8a",
+            .hash = "sokolshdc-0.1.0-r2KZDtYXlQS2C2CNekXgsRqhyKk5JtGzBlFBTMX27o0f",
         },
     },
 
diff --git a/shd/main.glsl b/shd/main.glsl
new file mode 100644 (file)
index 0000000..c6ec364
--- /dev/null
@@ -0,0 +1,301 @@
+// === combine ===
+@vs vs_combine
+const vec2 positions[4] = { vec2(-1, 1), vec2(1, 1), vec2(1, -1), vec2(-1, -1) };
+const vec2 uvs[4] = { vec2(0, 1), vec2(1, 1), vec2(1, 0), vec2(0, 0) };
+const int indices[6] = { 0, 1, 2, 0, 2, 3 };
+
+layout(binding=0) uniform Game
+{
+    vec2 screen;
+    vec2 cam;
+};
+
+out vec2 uv;
+
+void main() {
+    int idx = indices[gl_VertexIndex];
+    vec2 pos = positions[idx];
+    uv = uvs[idx];
+
+    gl_Position = vec4(pos, 1, 1);
+}
+@end
+
+@fs fs_combine
+layout(binding=0) uniform texture2D tex_tex;
+layout(binding=1) uniform texture2D tex_shadow;
+layout(binding=2) uniform texture2D tex_light;
+layout(binding=0) uniform sampler smp;
+
+in vec2 uv;
+out vec4 frag_color;
+
+
+void main() {
+    vec4 tb = vec4(0.8,0.3,0.3,1);
+    if (mod(floor(uv.x*10), 2) != mod(floor(uv.y*10),2)) {
+        tb = vec4(0.3,0.3,0.8,1);
+    }
+    vec4 tt = texture(sampler2D(tex_tex, smp), uv);
+    vec4 ts = texture(sampler2D(tex_shadow, smp), uv);
+    vec4 tl = texture(sampler2D(tex_light, smp), uv);
+    
+    vec3 l = tt.a * (tt.rgb * tl.rgb) + (1-tt.a) * (tl.rgb * tl.a);
+    l = ts.a * (l * ts.rgb) + (1-ts.a) * (l);
+    frag_color = vec4(l, 1);
+
+    vec3 a = (tb.rgb + tl.rgb) * tt.rgb * ts.rgb; // + tl.rgb * tl.a;
+    frag_color = vec4(a, 1);
+
+
+    vec3 bg = tb.rgb * tl.rgb;
+    vec3 bg_s = bg * (1-ts.a) + bg * ts.rgb * 0.5 * ts.a;
+    vec3 t = tt.rgb * tl.rgb;
+    vec3 t_s = t * (1-ts.a) + t * ts.rgb * 0.5 * ts.a;
+    vec3 bg_s_t_s = bg_s * (1-tt.a) + t_s * tt.a;
+    frag_color = vec4(bg_s_t_s, 1);
+
+    // frag_color = vec4(tl);
+}
+@end
+
+@program combine vs_combine fs_combine
+
+
+// === tex ===
+@vs vs_tex
+const vec2 positions[4] = { vec2(0, 1), vec2(1, 1), vec2(1, 0), vec2(0, 0) };
+const vec2 uvs[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) };
+const int indices[6] = { 0, 1, 2, 0, 2, 3 };
+
+layout(binding=0) uniform Game
+{
+    vec2 screen;
+    vec2 cam;
+};
+
+in vec2 inst_pos;
+in ivec2 inst_size;
+out vec2 uv;
+
+void main() {
+    int idx = indices[gl_VertexIndex];
+    vec2 pos = positions[idx];
+    uv = uvs[idx];
+
+    vec2 inst_pos_abs = inst_pos + inst_size * pos - cam - screen / 2;
+    gl_Position = vec4(inst_pos_abs / screen * 2, 1, 1);
+}
+@end
+
+@fs fs_tex
+layout(binding=0) uniform texture2D tex;
+layout(binding=0) uniform sampler smp;
+
+in vec2 uv;
+out vec4 frag_color;
+
+void main() {
+    vec4 tex_color = texture(sampler2D(tex, smp), uv);
+    if (tex_color.a < 1)
+        discard;
+    frag_color = tex_color;
+}
+@end
+
+@program tex vs_tex fs_tex
+
+
+// === shadows ===
+@vs vs_shadow
+const vec2 positions[3] = { vec2(0, 1), vec2(1, 1), vec2(0.5, 0) };
+const vec2 uvs[3] = { vec2(0, 0), vec2(1, 0), vec2(0.5, 1) };
+
+layout(binding=0) uniform Game
+{
+    vec2 screen;
+    vec2 cam;
+};
+
+layout(binding=1) uniform Light
+{
+    vec3 color;
+    vec2 pos;
+    float reach;
+    float radius_glow;
+    float intensity;
+    float shadow_brightness;
+} light;
+
+in vec2 inst_pos;
+in ivec2 inst_size;
+out vec2 uv;
+out vec3 light_color;
+out float shadow_brightness;
+
+// https://gist.github.com/yiwenl/3f804e80d0930e34a0b33359259b556c
+vec2 rotate(vec2 v, float a) {
+       float s = sin(a);
+       float c = cos(a);
+       mat2 m = mat2(c, s, -s, c);
+       return m * v;
+}
+
+void main() {
+    int idx = gl_VertexIndex;
+    vec2 pos = positions[idx];
+    uv = uvs[idx];
+    light_color = light.color;
+    shadow_brightness = light.shadow_brightness;
+
+    if (idx == 2) { // keep pos for base
+        vec2 inst_pos_abs = inst_pos + inst_size * pos - cam - screen / 2;
+        gl_Position = vec4(inst_pos_abs / screen * 2, 1, 1);
+    }
+    else {
+        vec2 anchor = vec2(0.5, 0);
+        vec2 anchor_abs = inst_pos + inst_size * anchor - cam - screen / 2;
+        vec2 light_pos_abs = light.pos - cam - screen / 2;
+        vec2 dir = anchor_abs - light_pos_abs;
+
+        vec2 left_right[] = {
+            rotate(dir, 1.5708),
+            rotate(dir, -1.5708),
+        };
+
+        float light_radius_factor = 1.5 / log(light.reach);
+
+        vec2 inst_pos_abs = anchor_abs + dir + left_right[idx] * light_radius_factor;
+        gl_Position = vec4(inst_pos_abs / screen * 2, 1, 1);
+    }
+}
+@end
+
+@fs fs_shadow
+layout(binding=0) uniform texture2D tex;
+layout(binding=0) uniform sampler smp;
+
+in vec2 uv;
+in vec3 light_color;
+in float shadow_brightness;
+out vec4 frag_color;
+
+vec3 brighten(vec3 c, float f) {
+    vec3 d = vec3(1) - c;
+    return c + d * f;
+}
+
+vec4 blur(float radius, float resolution) {
+    float blur = radius/resolution; 
+    
+    float hstep = 1;
+    float vstep = 1;
+    
+    vec4 sum = vec4(0.0);
+    
+    sum += texture(sampler2D(tex, smp), vec2(uv.x - 4.0*blur*hstep, uv.y - 4.0*blur*vstep)) * 0.0162162162;
+    sum += texture(sampler2D(tex, smp), vec2(uv.x - 3.0*blur*hstep, uv.y - 3.0*blur*vstep)) * 0.0540540541;
+    sum += texture(sampler2D(tex, smp), vec2(uv.x - 2.0*blur*hstep, uv.y - 2.0*blur*vstep)) * 0.1216216216;
+    sum += texture(sampler2D(tex, smp), vec2(uv.x - 1.0*blur*hstep, uv.y - 1.0*blur*vstep)) * 0.1945945946;
+    
+    sum += texture(sampler2D(tex, smp), vec2(uv.x, uv.y)) * 0.2270270270;
+    
+    sum += texture(sampler2D(tex, smp), vec2(uv.x + 1.0*blur*hstep, uv.y + 1.0*blur*vstep)) * 0.1945945946;
+    sum += texture(sampler2D(tex, smp), vec2(uv.x + 2.0*blur*hstep, uv.y + 2.0*blur*vstep)) * 0.1216216216;
+    sum += texture(sampler2D(tex, smp), vec2(uv.x + 3.0*blur*hstep, uv.y + 3.0*blur*vstep)) * 0.0540540541;
+    sum += texture(sampler2D(tex, smp), vec2(uv.x + 4.0*blur*hstep, uv.y + 4.0*blur*vstep)) * 0.0162162162;
+
+    return sum;
+}
+
+void main() {
+    vec4 tex_color = blur(10, 1);
+    tex_color = texture(sampler2D(tex, smp), uv);
+    
+    vec3 brighter = brighten(light_color, shadow_brightness);
+    frag_color = vec4(brighter * tex_color.a + vec3(1) * (1-tex_color.a), tex_color.a);
+}
+@end
+
+@program shadow vs_shadow fs_shadow
+
+
+// === lights ===
+@vs vs_light
+const vec2 positions[4] = { vec2(-0.5, 0.5), vec2(0.5, 0.5), vec2(0.5, -0.5), vec2(-0.5, -0.5) };
+const int indices[6] = { 0, 1, 2, 0, 2, 3 };
+
+layout(binding=0) uniform Game
+{
+    vec2 screen;
+    vec2 cam;
+};
+
+layout(binding=1) uniform Light
+{
+    vec3 color;
+    vec2 pos;
+    float reach;
+    float radius_glow;
+    float intensity;
+    float shadow_brightness;
+} light;
+
+out vec3 light_col;
+out vec2 light_pos;
+out float light_rea;
+out float light_glo;
+out float light_intensity;
+out vec2 fragpos;
+
+void main() {
+    int idx = indices[gl_VertexIndex];
+    vec2 pos = positions[idx];
+
+    vec2 inst_pos_ap = light.pos + light.reach * 2 * pos;
+    vec2 inst_pos_rp = inst_pos_ap - cam - screen / 2;
+    vec2 inst_pos_rw = inst_pos_rp / screen * 2;
+    gl_Position = vec4(inst_pos_rw, 1, 1);
+
+    light_col = light.color;
+    light_pos = light.pos;
+    light_rea = light.reach;
+    light_glo = light.radius_glow;
+    light_intensity = light.intensity;
+    fragpos = inst_pos_ap;
+}
+@end
+
+@fs fs_light
+layout(binding=0) uniform Game
+{
+    vec2 screen;
+    vec2 cam;
+};
+
+in vec3 light_col;
+in vec2 light_pos;
+in float light_rea;
+in float light_glo;
+in float light_intensity;
+in vec2 fragpos;
+out vec4 frag_color;
+
+float dist(vec2 a, vec2 b) {
+    float d1 = a.x - b.x;
+    float d2 = a.y - b.y;
+    return sqrt(d1*d1 + d2*d2);
+}
+
+void main() {
+    // vec2 frag_coord_abs = gl_FragCoord.xy * v_screen + v_cam + v_screen / 2;
+    // float factor = dist(light_pos, frag_coord_abs) / light_rad;
+    float factor_rea = 1 - (dist(light_pos, fragpos) / light_rea);
+    float factor_glo = 1 - (dist(light_pos, fragpos) / light_glo);
+    if (factor_rea > 1.0) discard;
+    frag_color = vec4(light_col.rgb * factor_rea, factor_glo * light_intensity);
+    // frag_color = vec4(vec3(mod(factor, 1.0)) * light_col, factor);
+}
+@end
+
+@program light vs_light fs_light
diff --git a/shd/quad.glsl b/shd/quad.glsl
deleted file mode 100644 (file)
index 7f073c4..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-/* quad vertex shader */
-@vs vs
-const vec2 positions[4] = { vec2(0, 1), vec2(1, 1), vec2(1, 0), vec2(0, 0) };
-const vec2 uvs[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) };
-const int indices[6] = { 0, 1, 2, 0, 2, 3 };
-
-layout(binding=0) uniform Game
-{
-    vec2 screen;
-    vec2 cam;
-};
-
-in vec2 inst_pos;
-in ivec2 inst_size;
-out vec2 uv;
-
-void main() {
-    int idx = indices[gl_VertexIndex];
-    vec2 pos = positions[idx];
-    uv = uvs[idx];
-
-    vec2 inst_pos_abs = inst_pos + inst_size * pos - cam - screen / 2;
-    gl_Position = vec4(inst_pos_abs / screen * 2, 1, 1);
-}
-@end
-
-/* quad fragment shader */
-@fs fs
-layout(binding=0) uniform texture2D tex;
-layout(binding=0) uniform sampler smp;
-
-in vec2 uv;
-out vec4 frag_color;
-
-void main() {
-    vec4 tex_color = texture(sampler2D(tex, smp), uv);
-    if (tex_color.a < 1)
-        discard;
-    frag_color = tex_color;
-}
-@end
-
-/* quad shader program */
-@program quad vs fs
-
-
-
-
-
-@vs vs_shadow
-const vec2 positions[4] = { vec2(0, 1), vec2(1, 1), vec2(1, 0), vec2(0, 0) };
-const vec2 uvs[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) };
-const int indices[6] = { 0, 1, 2, 0, 2, 3 };
-
-layout(binding=0) uniform Game
-{
-    vec2 screen;
-    vec2 cam;
-};
-
-layout(binding=1) uniform Light
-{
-    vec2 light_pos;
-};
-
-in vec2 inst_pos;
-in ivec2 inst_size;
-out vec2 uv;
-
-vec2 rotate(vec2 v, float a) {
-       float s = sin(a);
-       float c = cos(a);
-       mat2 m = mat2(c, s, -s, c);
-       return m * v;
-}
-
-void main() {
-    int idx = indices[gl_VertexIndex];
-    vec2 pos = positions[idx];
-    uv = uvs[idx];
-
-    if (idx == 2 || idx == 3) { // keep pos for lower vertices (base)
-        vec2 inst_pos_abs = inst_pos + inst_size * pos - cam - screen / 2;
-        gl_Position = vec4(inst_pos_abs / screen * 2, 1, 1);
-    }
-    else {
-        vec2 anchor = vec2(0.5, 0);
-        vec2 anchor_abs = inst_pos + inst_size * anchor - cam - screen / 2;
-        vec2 light_pos_abs = light_pos - cam - screen / 2;
-        vec2 dir = anchor_abs - light_pos_abs;
-
-        vec2 left_right[] = {
-            rotate(dir, 1.5708),
-            rotate(dir, -1.5708),
-        };
-
-        vec2 inst_pos_abs = anchor_abs + dir + left_right[idx] / 2;
-        gl_Position = vec4(inst_pos_abs / screen * 2, 1, 1);
-    }
-}
-@end
-
-@fs fs_shadow
-layout(binding=0) uniform texture2D tex;
-layout(binding=0) uniform sampler smp;
-
-in vec2 uv;
-out vec4 frag_color;
-
-void main() {
-    vec4 tex_color = texture(sampler2D(tex, smp), uv);
-    // if (tex_color.a < 1)
-    //     discard;
-    frag_color = vec4(0, 0, 0, 1);
-}
-@end
-
-@program shadow vs_shadow fs_shadow
-
-
-
-@vs vs_light
-const vec2 positions[4] = { vec2(-0.5, 0.5), vec2(0.5, 0.5), vec2(0.5, -0.5), vec2(-0.5, -0.5) };
-const int indices[6] = { 0, 1, 2, 0, 2, 3 };
-
-layout(binding=0) uniform Game
-{
-    vec2 screen;
-    vec2 cam;
-};
-
-in vec2 light_pos;
-
-void main() {
-    int idx = indices[gl_VertexIndex];
-    vec2 pos = positions[idx];
-
-    vec2 light_size = vec2(10, 10);
-
-    vec2 inst_pos_abs = light_pos + light_size * pos - cam - screen / 2;
-    gl_Position = vec4(inst_pos_abs / screen * 2, 1, 1);
-}
-@end
-
-@fs fs_light
-out vec4 frag_color;
-
-void main() {
-    frag_color = vec4(1, 0, 0, 1);
-}
-@end
-
-@program light vs_light fs_light
index 4813e6909aee5ec0e9a913d2d3a6b794ce8b7f24..bf77621e3e15044d31c4c1fd1eb434f579d8c34c 100644 (file)
@@ -1,61 +1,11 @@
-const std = @import("std");
-
-const ig = @import("cimgui_docking");
-const sokol = @import("sokol");
-const slog = sokol.log;
-const sg = sokol.gfx;
-const sapp = sokol.app;
-const sglue = sokol.glue;
-const sfetch = sokol.fetch;
-const simgui = sokol.imgui;
-
-const stbi = @cImport({
-    @cInclude("stb_image.h");
-});
-
-const shd = @import("shd/quad.glsl.zig");
-
-const state = struct {
-    const MaxSprites = 100;
-    const MaxLights = 100;
-
-    var pass_action: sg.PassAction = .{};
-    var bind: sg.Bindings = .{};
-    var pip: sg.Pipeline = .{};
-
-    var pip_shadow: sg.Pipeline = .{};
-    
-    var bind_light: sg.Bindings = .{};
-    var pip_light: sg.Pipeline = .{}; 
-
-    var img_buf: [1024 * 1024]u8 = undefined;
-
-    var cam = [2]f32{ -300, -300 };
-
-    var sprites_buf: [MaxSprites]Sprite = undefined;
-    var sprites = std.ArrayListUnmanaged(Sprite).initBuffer(&state.sprites_buf);
-    var lights_buf: [MaxLights]Light = undefined;
-    var lights = std.ArrayListUnmanaged(Light).initBuffer(&state.lights_buf);
-};
-
-const Sprite = struct {
-    texid: usize = shd.IMG_tex,
-    pos: [2]f32 = [_]f32{ 0, 0 },
-    size: [2]i32 = [_]i32{ 300, 300 },
-};
-
-const Light = struct {
-    pos: [2]f32 = [_]f32{ 0, 0 },
-};
-
 export fn init() void {
     // initialize sokol-gfx
     sg.setup(.{
         .buffer_pool_size = 8,
-        .image_pool_size = 4,
+        .image_pool_size = 16,
         .shader_pool_size = 8,
         .pipeline_pool_size = 8,
-        .attachments_pool_size = 1,
+        .view_pool_size = 16,
         .environment = sglue.environment(),
         .logger = .{ .func = slog.func },
     });
@@ -65,48 +15,142 @@ export fn init() void {
         .num_lanes = 1,
         .logger = .{ .func = slog.func },
     });
-
-    // initialize sokol-imgui
+    stime.setup();
     simgui.setup(.{
         .logger = .{ .func = slog.func },
     });
     ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable;
 
     // initial clear color
-    state.pass_action.colors[0] = .{
+    state.shaders.tex.pass.action.colors[0] = sg.ColorAttachmentAction{
+        .load_action = .CLEAR,
+        .store_action = .STORE,
+        .clear_value = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 },
+    };
+    state.shaders.shadow.pass.action.colors[0] = sg.ColorAttachmentAction{
         .load_action = .CLEAR,
-        .clear_value = .{ .r = 0.0, .g = 0.5, .b = 1.0, .a = 1.0 },
+        .store_action = .STORE,
+        .clear_value = .{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 0.0 },
     };
+    state.shaders.light.pass.action.colors[0] = sg.ColorAttachmentAction{
+        .load_action = .CLEAR,
+        .store_action = .STORE,
+        .clear_value = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 },
+    };
+
+    // combining pass
+    state.shaders.tex.img = sg.makeImage(.{
+        .usage = .{ .color_attachment = true },
+        .width = state.OffscreenWidth,
+        .height = state.OffscreenHeight,
+        .sample_count = state.OffscreenSampleCount,
+        .pixel_format = .RGBA8,
+    });
+    state.shaders.tex.depth = sg.makeImage(.{
+        .usage = .{ .depth_stencil_attachment = true },
+        .width = state.OffscreenWidth,
+        .height = state.OffscreenHeight,
+        .sample_count = state.OffscreenSampleCount,
+        .pixel_format = .DEPTH,
+    });
+    state.shaders.shadow.img = sg.makeImage(.{
+        .usage = .{ .color_attachment = true },
+        .width = state.OffscreenWidth,
+        .height = state.OffscreenHeight,
+        .sample_count = state.OffscreenSampleCount,
+        .pixel_format = .RGBA8,
+    });
+    state.shaders.shadow.depth = sg.makeImage(.{
+        .usage = .{ .depth_stencil_attachment = true },
+        .width = state.OffscreenWidth,
+        .height = state.OffscreenHeight,
+        .sample_count = state.OffscreenSampleCount,
+        .pixel_format = .DEPTH,
+    });
+    state.shaders.light.img = sg.makeImage(.{
+        .usage = .{ .color_attachment = true },
+        .width = state.OffscreenWidth,
+        .height = state.OffscreenHeight,
+        .sample_count = state.OffscreenSampleCount,
+        .pixel_format = .RGBA8,
+    });
+    state.shaders.light.depth = sg.makeImage(.{
+        .usage = .{ .depth_stencil_attachment = true },
+        .width = state.OffscreenWidth,
+        .height = state.OffscreenHeight,
+        .sample_count = state.OffscreenSampleCount,
+        .pixel_format = .DEPTH,
+    });
+
+    state.shaders.combine.bind.views[shd.VIEW_tex_tex] = sg.makeView(.{
+        .texture = .{ .image = state.shaders.tex.img },
+    });
+    state.shaders.combine.bind.views[shd.VIEW_tex_shadow] = sg.makeView(.{
+        .texture = .{ .image = state.shaders.shadow.img },
+    });
+    state.shaders.combine.bind.views[shd.VIEW_tex_light] = sg.makeView(.{
+        .texture = .{ .image = state.shaders.light.img },
+    });
+    state.shaders.combine.bind.samplers[shd.SMP_smp] = sg.makeSampler(.{
+        .min_filter = sg.Filter.LINEAR,
+        .mag_filter = sg.Filter.LINEAR,
+    });
 
-    // default pass
-    state.bind.images[shd.IMG_tex] = sg.allocImage();
+    state.shaders.combine.pip = sg.makePipeline(.{
+        .shader = sg.makeShader(shd.combineShaderDesc(sg.queryBackend())),
+        .depth = .{
+            .compare = .LESS_EQUAL,
+            .write_enabled = true,
+        },
+    });
 
-    state.bind.samplers[shd.SMP_smp] = sg.makeSampler(.{
+    // texture pass
+    state.img = sg.allocImage();
+    state.shaders.tex.pass.attachments.colors[0] = sg.makeView(.{
+        .color_attachment = .{ .image = state.shaders.tex.img },
+    });
+    state.shaders.tex.pass.attachments.depth_stencil = sg.makeView(.{
+        .depth_stencil_attachment = .{ .image = state.shaders.tex.depth },
+    });
+    state.shaders.tex.bind.views[shd.VIEW_tex] = sg.allocView();
+    state.shaders.tex.bind.samplers[shd.SMP_smp] = sg.makeSampler(.{
         .min_filter = sg.Filter.NEAREST,
         .mag_filter = sg.Filter.NEAREST,
+        .wrap_u = sg.Wrap.CLAMP_TO_EDGE,
+        .wrap_v = sg.Wrap.CLAMP_TO_EDGE,
     });
 
     // dynamic vertex buffer for instancing data
-    state.bind.vertex_buffers[0] = sg.makeBuffer(.{
+    state.shaders.tex.bind.vertex_buffers[0] = sg.makeBuffer(.{
         .usage = .{ .stream_update = true },
         .size = state.MaxSprites * @sizeOf(Sprite),
     });
 
     // a shader and pipeline state object
-    state.pip = sg.makePipeline(.{
-        .shader = sg.makeShader(shd.quadShaderDesc(sg.queryBackend())),
+    state.shaders.tex.pip = sg.makePipeline(.{
+        .shader = sg.makeShader(shd.texShaderDesc(sg.queryBackend())),
         .layout = init: {
             var l = sg.VertexLayoutState{};
             l.buffers[0].step_func = .PER_INSTANCE;
-            l.attrs[shd.ATTR_quad_inst_pos] = .{ .format = .FLOAT2, .offset = @offsetOf(Sprite, "pos") };
-            l.attrs[shd.ATTR_quad_inst_size] = .{ .format = .INT2, .offset = @offsetOf(Sprite, "size") };
+            l.attrs[shd.ATTR_tex_inst_pos] = .{ .format = .FLOAT2, .offset = @offsetOf(Sprite, "pos") };
+            l.attrs[shd.ATTR_tex_inst_size] = .{ .format = .INT2, .offset = @offsetOf(Sprite, "size") };
             l.buffers[0].stride = @sizeOf(Sprite);
             break :init l;
         },
+        .depth = .{
+            .compare = .LESS_EQUAL,
+            .write_enabled = true,
+        },
     });
 
     // shadow pass
-    state.pip_shadow = sg.makePipeline(.{
+    state.shaders.shadow.pass.attachments.colors[0] = sg.makeView(.{
+        .color_attachment = .{ .image = state.shaders.shadow.img },
+    });
+    state.shaders.shadow.pass.attachments.depth_stencil = sg.makeView(.{
+        .depth_stencil_attachment = .{ .image = state.shaders.shadow.depth },
+    });
+    state.shaders.shadow.pip = sg.makePipeline(.{
         .shader = sg.makeShader(shd.shadowShaderDesc(sg.queryBackend())),
         .layout = init: {
             var l = sg.VertexLayoutState{};
@@ -116,22 +160,58 @@ export fn init() void {
             l.buffers[0].stride = @sizeOf(Sprite);
             break :init l;
         },
+        .depth = .{
+            .compare = .LESS_EQUAL,
+            .write_enabled = true,
+        },
+        .colors = init: {
+            var c: [4]sg.ColorTargetState = @splat(.{});
+            c[0] = .{
+                .blend = .{
+                    .enabled = true,
+                    .src_factor_rgb = .DST_COLOR,
+                    .dst_factor_rgb = .ZERO,
+                    .op_rgb = .ADD,
+                    .src_factor_alpha = .ONE,
+                    .dst_factor_alpha = .ONE,
+                    .op_alpha = .ADD,
+                },
+            };
+            break :init c;
+        },
     });
 
     // light pass
-    state.bind_light.vertex_buffers[0] = sg.makeBuffer(.{
+    state.shaders.light.pass.attachments.colors[0] = sg.makeView(.{
+        .color_attachment = .{ .image = state.shaders.light.img },
+    });
+    state.shaders.light.pass.attachments.depth_stencil = sg.makeView(.{
+        .depth_stencil_attachment = .{ .image = state.shaders.light.depth },
+    });
+    state.shaders.light.bind.vertex_buffers[0] = sg.makeBuffer(.{
         .usage = .{ .stream_update = true },
-        .size = state.MaxLights * @sizeOf(Light),
+        .size = state.MaxLights * @sizeOf(shd.Light),
     });
 
-    state.pip_light = sg.makePipeline(.{
+    state.shaders.light.pip = sg.makePipeline(.{
         .shader = sg.makeShader(shd.lightShaderDesc(sg.queryBackend())),
-        .layout = init: {
-            var l = sg.VertexLayoutState{};
-            l.buffers[0].step_func = .PER_INSTANCE;
-            l.attrs[shd.ATTR_light_light_pos] = .{ .format = .FLOAT2, .offset = @offsetOf(Light, "pos") };
-            l.buffers[0].stride = @sizeOf(Light);
-            break :init l;
+        .depth = .{
+            .compare = .LESS_EQUAL,
+            .write_enabled = true,
+        },
+        .blend_color = sg.Color{ .r=0.5,.g=0.5,.b=0.5,.a = 0.5 },
+        .colors = init: {
+            var c: [4]sg.ColorTargetState = @splat(.{});
+            c[0] = .{
+                .blend = .{
+                    .enabled = true,
+                    .src_factor_rgb = .SRC_COLOR,
+                    .dst_factor_rgb = .ONE_MINUS_SRC_COLOR,
+                    .src_factor_alpha = .ONE,
+                    .dst_factor_alpha = .ONE,
+                },
+            };
+            break :init c;
         },
     });
 
@@ -142,9 +222,10 @@ export fn init() void {
                 var w: c_int = 0;
                 var h: c_int = 0;
                 var n: c_int = 0;
+                // TODO: free memory?
                 const pixels = stbi.stbi_load_from_memory(@ptrCast(res.*.data.ptr.?), @intCast(res.*.data.size), &w, &h, &n, 4);
                 if (pixels != null) {
-                    sg.initImage(state.bind.images[shd.IMG_tex], .{
+                    sg.initImage(state.img, .{
                         .width = w,
                         .height = h,
                         .pixel_format = sg.PixelFormat.RGBA8,
@@ -157,6 +238,9 @@ export fn init() void {
                             break :init i;
                         },
                     });
+                    sg.initView(state.shaders.tex.bind.views[shd.VIEW_tex], .{
+                        .texture = .{ .image = state.img },
+                    });
                 }
             }
         }
@@ -168,12 +252,33 @@ export fn init() void {
     });
 
     state.sprites.appendAssumeCapacity(.{});
-    state.lights.appendAssumeCapacity(.{ .pos = [_]f32{ 150, -300 } });
+    state.lights.appendAssumeCapacity(DefaultLight);
+    state.lights.appendAssumeCapacity(DefaultLight);
+    state.lights.items[0].color = [_]f32{ 0.8, 1, 0.8 };
+}
+
+export fn cleanup() void {
+    simgui.shutdown();
+    sfetch.shutdown();
+    sg.shutdown();
+}
+
+export fn event(ev: [*c]const sapp.Event) void {
+    // forward input events to sokol-imgui
+    _ = simgui.handleEvent(ev.*);
+
+    switch (ev.*.type) {
+        .MOUSE_MOVE => {
+            state.lights.items[0].pos[0] = ev.*.mouse_x + state.cam[0];
+            state.lights.items[0].pos[1] = (sapp.heightf() - ev.*.mouse_y) + state.cam[1];
+        },
+        else => {},
+    }
 }
 
 fn ui() void {
     if (ig.igBegin("Hello Dear ImGui!", null, ig.ImGuiWindowFlags_None)) {
-        _ = ig.igColorEdit3("Background", &state.pass_action.colors[0].clear_value.r, ig.ImGuiColorEditFlags_None);
+        ig.igLabelText("fps", "%d", state.fps);
         _ = ig.igDragFloat2("cam", &state.cam);
 
         {
@@ -204,7 +309,7 @@ fn ui() void {
         {
             ig.igPushIDPtr(state.lights.items.ptr);
             if (ig.igButton("+")) {
-                _ = state.lights.appendAssumeCapacity(.{});
+                _ = state.lights.appendAssumeCapacity(DefaultLight);
             }
 
             var i: u16 = 0;
@@ -213,8 +318,18 @@ fn ui() void {
                 i += 1;
                 ig.igPushIDInt(i);
                 ig.igPushItemWidth(100);
+                _ = ig.igColorEdit3("color", &light.color, ig.ImGuiColorEditFlags_None);
+                ig.igSameLine();
                 _ = ig.igDragFloat2("pos", &light.pos);
                 ig.igSameLine();
+                _ = ig.igDragFloat("reach", &light.reach);
+                ig.igSameLine();
+                _ = ig.igDragFloat("radius_glow", &light.radius_glow);
+                ig.igSameLine();
+                _ = ig.igDragFloatEx("intensity", &light.intensity, 0.01, 0, 1, "%.3f", ig.ImGuiSliderFlags_None);
+                ig.igSameLine();
+                _ = ig.igDragFloatEx("shadow_brightness", &light.shadow_brightness, 0.01, 0, 1, "%.3f", ig.ImGuiSliderFlags_None);
+                ig.igSameLine();
                 if (ig.igButton("-")) {
                     i -= 1;
                     _ = state.lights.swapRemove(i);
@@ -229,8 +344,18 @@ fn ui() void {
 }
 
 export fn frame() void {
+    // sfetch
     sfetch.dowork();
 
+    // fps
+    const now = stime.now();
+    if (stime.ms(stime.diff(now, state.time)) >= 1000) {
+        state.fps = state.frames;
+        state.frames = 0;
+        state.time = now;
+    }
+    state.frames += 1;
+
     const width = sapp.width();
     const height = sapp.height();
     const widthf = sapp.widthf();
@@ -245,32 +370,32 @@ export fn frame() void {
     });
     ui();
 
-    // update instance vertex buffer
-    sg.updateBuffer(state.bind.vertex_buffers[0], sg.asRange(state.sprites.items));
-    sg.updateBuffer(state.bind_light.vertex_buffers[0], sg.asRange(state.lights.items));
-
-    sg.beginPass(.{ .action = state.pass_action, .swapchain = sglue.swapchain() });
-
     // shadow pass
-    sg.applyPipeline(state.pip_shadow);
-    sg.applyBindings(state.bind);
+    sg.beginPass(state.shaders.shadow.pass);
+
+    sg.applyPipeline(state.shaders.shadow.pip);
+    sg.applyBindings(state.shaders.tex.bind);
 
     sg.applyUniforms(shd.UB_Game, sg.asRange(&shd.Game{
         .screen = [_]f32{ widthf, heightf },
         .cam = state.cam,
     }));
 
-    for (state.lights.items) |light| {
-        sg.applyUniforms(shd.UB_Light, sg.asRange(&shd.Light{
-            .light_pos = light.pos,
-        }));
+    for (state.lights.items) |*light| {
+        sg.applyUniforms(shd.UB_Light, sg.asRange(light));
 
-        sg.draw(0, 6, @truncate(state.sprites.items.len));
+        sg.draw(0, 3, @truncate(state.sprites.items.len));
     }
 
-    // default pass
-    sg.applyPipeline(state.pip);
-    sg.applyBindings(state.bind);
+    sg.endPass();
+
+    // texture pass
+    sg.beginPass(state.shaders.tex.pass);
+
+    sg.updateBuffer(state.shaders.tex.bind.vertex_buffers[0], sg.asRange(state.sprites.items));
+
+    sg.applyPipeline(state.shaders.tex.pip);
+    sg.applyBindings(state.shaders.tex.bind);
 
     sg.applyUniforms(shd.UB_Game, sg.asRange(&shd.Game{
         .screen = [_]f32{ widthf, heightf },
@@ -279,32 +404,136 @@ export fn frame() void {
 
     sg.draw(0, 6, @truncate(state.sprites.items.len));
 
+    sg.endPass();
+
     // light pass
-    sg.applyPipeline(state.pip_light);
-    sg.applyBindings(state.bind_light);
+    sg.beginPass(state.shaders.light.pass);
+
+    sg.updateBuffer(state.shaders.light.bind.vertex_buffers[0], sg.asRange(state.lights.items));
+
+    sg.applyPipeline(state.shaders.light.pip);
+    sg.applyBindings(state.shaders.light.bind);
+
+    sg.applyUniforms(shd.UB_Game, sg.asRange(&shd.Game{
+        .screen = [_]f32{ widthf, heightf },
+        .cam = state.cam,
+    }));
+
+    for (state.lights.items) |*light| {
+        sg.applyUniforms(shd.UB_Light, sg.asRange(light));
+
+        sg.draw(0, 6, 1);
+    }
+
+    sg.endPass();
+
+    // combine pass
+    sg.beginPass(.{ .action = state.shaders.combine.pass_action, .swapchain = sglue.swapchain() });
+
+    sg.applyPipeline(state.shaders.combine.pip);
+    sg.applyBindings(state.shaders.combine.bind);
 
     sg.applyUniforms(shd.UB_Game, sg.asRange(&shd.Game{
         .screen = [_]f32{ widthf, heightf },
         .cam = state.cam,
     }));
 
-    sg.draw(0, 6, @truncate(state.lights.items.len));
+    sg.draw(0, 6, 1);
 
     simgui.render();
     sg.endPass();
     sg.commit();
 }
 
-export fn cleanup() void {
-    simgui.shutdown();
-    sfetch.shutdown();
-    sg.shutdown();
-}
+const state = struct {
+    var fps: u64 = 0;
+    var frames: u64 = 0;
+    var time: u64 = 0;
 
-export fn event(ev: [*c]const sapp.Event) void {
-    // forward input events to sokol-imgui
-    _ = simgui.handleEvent(ev.*);
-}
+    const OffscreenWidth = 1920;
+    const OffscreenHeight = 1080;
+    const OffscreenSampleCount = 1;
+    const MaxSprites = 100;
+    const MaxLights = 100;
+
+    const shaders = struct {
+        const combine = struct {
+            var pass_action: sg.PassAction = .{};
+            var bind: sg.Bindings = .{};
+            var pip: sg.Pipeline = .{};
+        };
+        const tex = struct {
+            var pass: sg.Pass = .{};
+            var bind: sg.Bindings = .{};
+            var pip: sg.Pipeline = .{};
+            var img: sg.Image = .{};
+            var depth: sg.Image = .{};
+        };
+        const shadow = struct {
+            var pass: sg.Pass = .{};
+            // var bind: sg.Bindings = .{};
+            var pip: sg.Pipeline = .{};
+            var img: sg.Image = .{};
+            var depth: sg.Image = .{};
+        };
+        const light = struct {
+            var pass: sg.Pass = .{};
+            var bind: sg.Bindings = .{};
+            var pip: sg.Pipeline = .{};
+            var img: sg.Image = .{};
+            var depth: sg.Image = .{};
+        };
+    };
+
+    var img_buf: [1024 * 1024]u8 = undefined;
+    var img: sg.Image = undefined;
+
+    var cam = [2]f32{ -300, -300 };
+
+    var sprites_buf: [MaxSprites]Sprite = undefined;
+    var sprites = std.ArrayListUnmanaged(Sprite).initBuffer(&state.sprites_buf);
+    var lights_buf: [MaxLights]shd.Light = undefined;
+    var lights = std.ArrayListUnmanaged(shd.Light).initBuffer(&state.lights_buf);
+};
+
+const DefaultLight = shd.Light{
+    .pos = [_]f32{ 150, -300 },
+    .color = [_]f32{ 1, 0.8, 0 },
+    .reach = 500,
+    .radius_glow = 200,
+    .intensity = 1.0,
+    .shadow_brightness = 0.5,
+};
+
+const Sprite = struct {
+    texid: usize = shd.VIEW_tex,
+    pos: [2]f32 = [_]f32{ 0, 0 },
+    size: [2]i32 = [_]i32{ 300, 300 },
+};
+
+// const Light = struct {
+//     color: [3]f32 = [_]f32{ 1, 0, 0 },
+//     pos: [2]f32 = [_]f32{ 0, 0 },
+//     radius: f32 = 100,
+// };
+
+const std = @import("std");
+
+const ig = @import("cimgui_docking");
+const sokol = @import("sokol");
+const slog = sokol.log;
+const sg = sokol.gfx;
+const sapp = sokol.app;
+const stime = sokol.time;
+const sglue = sokol.glue;
+const sfetch = sokol.fetch;
+const simgui = sokol.imgui;
+
+const stbi = @cImport({
+    @cInclude("stb_image.h");
+});
+
+const shd = @import("shd/main.glsl.zig");
 
 pub fn main() void {
     sapp.run(.{