From 88d562e70a3e224bedf0b2081941c605937d3532 Mon Sep 17 00:00:00 2001 From: froxcey Date: Sun, 15 Sep 2024 08:42:52 +0800 Subject: [PATCH] First working version --- .gitignore | 5 + README.md | 48 ++++++++ assets/img/.keep | 0 build.zig | 82 +++++++++++++ build.zig.zon | 35 ++++++ src/main.zig | 14 +++ src/utils/guideline.txt | 10 ++ src/views/landmark.zig | 122 +++++++++++++++++++ src/views/main.zig | 253 ++++++++++++++++++++++++++++++++++++++++ src/views/shader.wgsl | 91 +++++++++++++++ 10 files changed, 660 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 assets/img/.keep create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/main.zig create mode 100644 src/utils/guideline.txt create mode 100644 src/views/landmark.zig create mode 100644 src/views/main.zig create mode 100644 src/views/shader.wgsl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2f029f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +assets/img + +.DS_Store +.zig-cache/ +zig-out/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe8384b --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# V2D Studio + +Cool wip stuff, build it yourself to see what this does. + +## Installation + +> Installation can sometimes take up to 20min depending on your hardware. Please be patient with the installation process. + +### MacOS + +Installing v2d is hastle free via [Homebrew](https://brew.sh) + my own repo + +```sh +brew tap chiissu/macchiato & brew install v2d +``` + +### Linux + +It is very possible to get this up and running on Linux. Since I cannot maintain a repository you'll have to do two things yourself: + +1. Build and install [libmediapipe](https://github.com/froxcey/libmediapipe) +2. Build v2d + +### Windows + +This project theoretically could work on Windows, but the upstream software, mediapipe, does not support hardware accelerated detection on Windows. Also, I don't have access to a Windows machine. There is no plan to provide official Windows support until dependency requirements are met. + +## Running + +Before you can do anything, you'll need to add your own asset `main.png` in `assets/img/` + +After finishing configurating, you can run `v2d` in the terminal to run the app. + +If you have any general question regarding the usage of v2d, feel free to contact [@froxcey](https://github.com/froxcey) + +## Related software + +You're welcome to add projects here, as long as it is primarily written in a [tier B language or above](https://github.com/Froxcey/Froxcey/blob/main/lang_tier.md) and source available. We encourage users to check out alternatives and choose what is the best for their own use case. + +## Future plan + +It would be cool to add WebRTP video streaming support + +## Copyright & Usage Guideline + +This software is not fully FOSS, please read [this usage guideline](./src/utils/guideline.txt). + +IMPORTANT COPY-LEFT NOTICE: By distributing modifications or derivative works, a perpetual, worldwide, non-exclusive, royalty-free license is granted to the original author to incorporate these modifications into the original software or any future versions. diff --git a/assets/img/.keep b/assets/img/.keep new file mode 100644 index 0000000..e69de29 diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..3e32b5c --- /dev/null +++ b/build.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // const utils = b.createModule(.{ .root_source_file = b.path("src/utils/main.zig") }); + + const exe = b.addExecutable(.{ + .name = "v2d", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const mach_dep = b.dependency("mach", .{ + .target = target, + .optimize = optimize, + .core = true, + .sysgpu = true, + .sysaudio = true, + }); + exe.root_module.addImport("mach", mach_dep.module("mach")); + + const zigimg_dep = b.dependency("zigimg", .{ + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("zigimg", zigimg_dep.module("zigimg")); + + const zigcv_dep = b.dependency("zigcv", .{ + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("zigcv", zigcv_dep.module("zigcv")); + + const imgui_dep = b.dependency("mach-imgui", .{ .target = target, .optimize = optimize }); + const imgui_module = b.addModule("mach-imgui", .{ + .root_source_file = imgui_dep.path("src/imgui.zig"), + .imports = &.{ + .{ .name = "mach", .module = mach_dep.module("mach") }, + }, + }); + exe.root_module.addImport("imgui", imgui_module); + // utils.addImport("imgui", imgui_module); + exe.linkLibrary(imgui_dep.artifact("imgui")); + + const nfd_dep = b.dependency("nfd", .{ + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("nfd", nfd_dep.module("nfd")); + // utils.addImport("nfd", nfd_dep.module("nfd")); + + // exe.root_module.addImport("$utils", utils); + + exe.linkSystemLibrary("libmediapipe"); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..960e092 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,35 @@ +.{ + .name = "v2d", + + .version = "0.0.0", + + .minimum_zig_version = "0.13.0", + + .dependencies = .{ + .mach = .{ + .url = "https://github.com/hexops/mach/archive/98801a258e4f59e1c464206684e7751818609f77.tar.gz", + .hash = "1220c62d964c12d2446d90a294e28f863b7f668dd5c3691e652f336119b2096d11f4", + }, + .zigimg = .{ + .url = "https://github.com/zigimg/zigimg/archive/d9dbbe22b5f7b5f1f4772169ed93ffeed8e8124d.zip", + .hash = "122013646f7038ecc71ddf8a0d7de346d29a6ec40140af57f838b0a975c69af512b0", + }, + .zigcv = .{ + .url = "https://github.com/Froxcey/zigcv/archive/refs/tags/0.13_compat_0.2.0.tar.gz", + .hash = "1220b283a1e91944d07b0018d4b59221a2031f342b55b062d170936dc413d098f214", + }, + .@"mach-imgui" = .{ + .url = "https://github.com/Froxcey/mach-imgui/archive/84efeaa62bff899475c0994dbf1372e30680dc10.zip", + .hash = "1220a733821fd9d7ad636d0616cea58e7c83d8db1971e694dc54edc1ff595257a4e5", + }, + .nfd = .{ + .url = "https://github.com/Froxcey/nfd-zig/archive/f6d36231ca34eeb7668c3672b7892a0490608171.tar.gz", + .hash = "122074ece772fc03abffa254b6e717aa3efc86832488954ac6a962e02a039d781ed1", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..020d308 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,14 @@ +const mach = @import("mach"); + +pub const modules = .{ + mach.Core, + @import("views/main.zig"), +}; + +pub fn main() !void { + // Initialize mach.Core + try mach.core.initModule(); + + // Main loop + while (try mach.core.tick()) {} +} diff --git a/src/utils/guideline.txt b/src/utils/guideline.txt new file mode 100644 index 0000000..953154c --- /dev/null +++ b/src/utils/guideline.txt @@ -0,0 +1,10 @@ +[Last updated: 30/Aug, 2024] + +This short and non-binding guideline outlines how to use our intellectual property to help creators and consumers engage with our work with mutual expectations and common sense. + +1. Our work must not be used for any illegal activity or with any malicious intent (like harming others, infringing on intellectual property, impersonating, or promoting hate) +2. Credit us for the usage of our work +3. Modification to our work must be shared with us to benefit everyone +4. Commercial distribution of our and your derivative work must be done in person, using a physical medium, and without reliance on any subscription service or rights management +5. Serious violation of this guideline may result in legal actions +6. We may update this guideline at any time without notice, so please review this periodically diff --git a/src/views/landmark.zig b/src/views/landmark.zig new file mode 100644 index 0000000..4f432cd --- /dev/null +++ b/src/views/landmark.zig @@ -0,0 +1,122 @@ +const std = @import("std"); +const mediapipe = @cImport({ + @cInclude("mediapipe.h"); +}); +const cv = @import("zigcv"); + +const resource_dir = "/opt/homebrew/opt/libmediapipe/lib/data"; + +const Self = @This(); +const InitConfig = struct { + camera_id: u8 = 0, + use_gpu: bool = true, +}; + +webcam: cv.VideoCapture, +frame: cv.Mat, +instance: *mediapipe.mp_instance, +pose_landmarks_poller: *mediapipe.mp_poller, +face_landmarks_poller: *mediapipe.mp_poller, + +pub fn init(config: InitConfig) !Self { + var webcam = try cv.VideoCapture.init(); + try webcam.openDevice(config.camera_id); + + mediapipe.mp_set_resource_dir(resource_dir); + const builder = mediapipe.mp_create_instance_builder(resource_dir ++ "/mediapipe/modules/holistic_landmark/holistic_landmark_gpu.binarypb", "image"); + mediapipe.mp_add_side_packet(builder, "num_poses", mediapipe.mp_create_packet_int(1)); + mediapipe.mp_add_side_packet(builder, "model_complexity", mediapipe.mp_create_packet_int(2)); + mediapipe.mp_add_side_packet(builder, "refine_face_landmarks", mediapipe.mp_create_packet_bool(true)); + mediapipe.mp_add_side_packet(builder, "use_prev_landmarks", mediapipe.mp_create_packet_bool(true)); + + const instance = mediapipe.mp_create_instance(builder); + checkNull(instance); + + const pose_landmarks_poller = mediapipe.mp_create_poller(instance, "pose_landmarks"); + checkNull(pose_landmarks_poller); + + const face_landmarks_poller = mediapipe.mp_create_poller(instance, "face_landmarks"); + + checkNull(face_landmarks_poller); + checkBool(mediapipe.mp_start(instance)); + + return Self{ + .webcam = webcam, + .instance = instance.?, + .pose_landmarks_poller = pose_landmarks_poller.?, + .face_landmarks_poller = face_landmarks_poller.?, + .frame = try cv.Mat.init(), + }; +} + +pub fn deinit(self: *Self) void { + mediapipe.mp_destroy_poller(self.pose_landmarks_poller); + mediapipe.mp_destroy_poller(self.face_landmarks_poller); + _ = mediapipe.mp_destroy_instance(self.instance); + self.frame.deinit(); + self.webcam.deinit(); +} + +pub const Result = struct { + rotation: f32, +}; +pub fn poll(self: *Self) !Result { + var frame = self.frame; + var rotation: f32 = 0; + + self.webcam.read(&frame) catch { + std.debug.print("capture failed", .{}); + std.posix.exit(1); + }; + + if (frame.isEmpty()) + return error.EmptyFrame; + + cv.cvtColor(frame, &frame, .bgr_to_bgra); + + const image = mediapipe.mp_image{ + .data = @ptrCast(frame.toBytes()), + .width = frame.cols(), + .height = frame.rows(), + .format = mediapipe.mp_image_format_srgba, + }; + + const p = mediapipe.mp_create_packet_image(image); + checkBool(mediapipe.mp_process(self.instance, p)); + checkBool(mediapipe.mp_wait_until_idle(self.instance)); + + if (mediapipe.mp_get_queue_size(self.pose_landmarks_poller) > 0) { + const packet = mediapipe.mp_poll_packet(self.pose_landmarks_poller); + defer mediapipe.mp_destroy_packet(packet); + const landmarks = mediapipe.mp_get_norm_landmarks(packet); + defer mediapipe.mp_destroy_landmarks(landmarks); + // do pose landmark logic + } + + if (mediapipe.mp_get_queue_size(self.face_landmarks_poller) > 0) { + const packet = mediapipe.mp_poll_packet(self.face_landmarks_poller); + defer mediapipe.mp_destroy_packet(packet); + const landmarks = mediapipe.mp_get_norm_landmarks(packet); + defer mediapipe.mp_destroy_landmarks(landmarks); + const nose = landmarks.*.elements[1]; + const cheek = landmarks.*.elements[411]; + rotation = std.math.atan2(cheek.y - nose.y, cheek.x - nose.x); + + // do face landmark logic + //drawLandmarks(&frame, landmarks); + } + + return Result{ + .rotation = rotation, + }; +} + +fn checkNull(result: anytype) void { + if (result == null) checkBool(false); +} +fn checkBool(result: bool) void { + if (!result) { + std.log.err("[Mediapipe] {s}", .{mediapipe.mp_get_last_error()}); + std.posix.exit(1); + } +} diff --git a/src/views/main.zig b/src/views/main.zig new file mode 100644 index 0000000..1f7f35d --- /dev/null +++ b/src/views/main.zig @@ -0,0 +1,253 @@ +const std = @import("std"); +const mach = @import("mach"); +const gpu = mach.gpu; +const zigimg = @import("zigimg"); +const landmark = @import("./landmark.zig"); + +pub const name = .app; +pub const Mod = mach.Mod(@This()); + +pub const systems = .{ + .init = .{ .handler = init }, + .after_init = .{ .handler = afterInit }, + .deinit = .{ .handler = deinit }, + .tick = .{ .handler = tick }, +}; + +title_timer: mach.Timer, +pipeline: *gpu.RenderPipeline, +landmarker: landmark, + +var binding0: *gpu.BindGroup = undefined; +var texture: *gpu.Texture = undefined; + +pub fn deinit(core: *mach.Core.Mod, game: *Mod) void { + texture.release(); + binding0.release(); + game.state().landmarker.deinit(); + game.state().pipeline.release(); + core.schedule(.deinit); +} + +fn init(game: *Mod, core: *mach.Core.Mod) !void { + core.schedule(.init); + game.schedule(.after_init); +} + +const Config = struct { + seg1: f32, + seg2: f32, +}; + +fn afterInit(game: *Mod, core: *mach.Core.Mod) !void { + const device: *gpu.Device = core.state().device; + const queue: *gpu.Queue = core.state().queue; + + // Create our shader module + const shader_module = device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); + defer shader_module.release(); + + // Blend state describes how rendered colors get blended + const blend = gpu.BlendState{ + .color = .{ .src_factor = .src_alpha, .dst_factor = .one_minus_src_alpha }, + }; + + // Color target describes e.g. the pixel format of the window we are rendering to. + const color_target = gpu.ColorTargetState{ + .format = core.get(core.state().main_window, .framebuffer_format).?, + .blend = &blend, + }; + + // Fragment state describes which shader and entrypoint to use for rendering fragments. + const fragment = gpu.FragmentState.init(.{ + .module = shader_module, + .entry_point = "frag_main", + .targets = &.{color_target}, + }); + + // Read the image file + const asset_name = "main"; + var file = try std.fs.cwd().openFile("assets/img/" ++ asset_name ++ ".png", .{}); + defer file.close(); + + var img = try zigimg.Image.fromFile(core.state().allocator, &file); + defer img.deinit(); + + const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) }; + + texture = device.createTexture(&.{ + .label = asset_name ++ ".loadTexture", + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true }, + }); + + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @as(u32, @intCast(img.width * 4)), + .rows_per_image = @as(u32, @intCast(img.height)), + }; + switch (img.pixels) { + .rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels), + .rgb24 => |pixels| { + const data = try rgb24ToRgba32(core.state().allocator, pixels); + defer data.deinit(core.state().allocator); + queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32); + }, + else => @panic("unsupported image color format"), + } + var texture_view = texture.createView(&.{ .label = "main" }); + defer texture_view.release(); + + // Create our render pipeline that will ultimately get pixels onto the screen. + const label = @tagName(name) ++ ".init"; + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ + .label = label, + .fragment = &fragment, + .vertex = gpu.VertexState{ + .module = shader_module, + .entry_point = "vertex_main", + }, + }; + const pipeline = device.createRenderPipeline(&pipeline_descriptor); + const layout0 = pipeline.getBindGroupLayout(0); + defer layout0.release(); + + var sampler = device.createSampler(&.{ .mag_filter = .nearest, .min_filter = .nearest }); + //var sampler = device.createSampler(&.{}); + defer sampler.release(); + + const config = Config{ + .seg1 = 0.21875, + .seg2 = 0.375, + }; + const config_buffer = device.createBuffer(&.{ + .label = "config", + .usage = .{ .uniform = true, .copy_dst = true }, + .size = @sizeOf(Config), + .mapped_at_creation = gpu.Bool32.false, + }); + queue.writeBuffer(config_buffer, 0, &std.mem.toBytes(config)); + + binding0 = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout0, + .entries = &.{ + gpu.BindGroup.Entry.sampler(0, sampler), + gpu.BindGroup.Entry.textureView(1, texture_view), + gpu.BindGroup.Entry.buffer(2, config_buffer, 0, @sizeOf(Config), 0), + }, + })); + + // Store our render pipeline in our module's state, so we can access it later on. + game.init(.{ + .title_timer = try mach.Timer.start(), + .pipeline = pipeline, + .landmarker = try landmark.init(.{ .camera_id = 2 }), + }); + try updateWindowTitle(core); + + core.schedule(.start); +} + +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage { + const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len); + var i: usize = 0; + while (i < in.len) : (i += 1) { + out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 }; + } + return out; +} + +fn tick(core: *mach.Core.Mod, game: *Mod) !void { + // TODO(important): event polling should occur in mach.Core module and get fired as ECS event. + // TODO(Core) + var iter = mach.core.pollEvents(); + while (iter.next()) |event| { + switch (event) { + .close => core.schedule(.exit), // Tell mach.Core to exit the app + else => {}, + } + } + + const device: *gpu.Device = core.state().device; + const queue: *gpu.Queue = core.state().queue; + + // Create variable binding + const result: landmark.Result = try game.state().landmarker.poll(); + const result_buffer = device.createBuffer(&.{ + .label = "result", + .usage = .{ .uniform = true, .copy_dst = true }, + .size = @sizeOf(landmark.Result), + .mapped_at_creation = gpu.Bool32.false, + }); + const layout1 = game.state().pipeline.getBindGroupLayout(1); + defer layout1.release(); + queue.writeBuffer(result_buffer, 0, &std.mem.toBytes(result)); + var binding1 = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{ + .layout = layout1, + .entries = &.{ + gpu.BindGroup.Entry.buffer(0, result_buffer, 0, @sizeOf(landmark.Result), 0), + }, + })); + defer binding1.release(); + + // Grab the back buffer of the swapchain + // TODO(Core) + const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + // Create a command encoder + const label = @tagName(name) ++ ".tick"; + const encoder = core.state().device.createCommandEncoder(&.{ .label = label }); + defer encoder.release(); + + // Begin render pass + const sky_blue_background = gpu.Color{ .r = 0.776, .g = 0.988, .b = 1, .a = 1 }; + const color_attachments = [_]gpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .clear_value = sky_blue_background, + .load_op = .clear, + .store_op = .store, + }}; + const render_pass = encoder.beginRenderPass(&gpu.RenderPassDescriptor.init(.{ + .label = label, + .color_attachments = &color_attachments, + })); + defer render_pass.release(); + + // Draw + render_pass.setPipeline(game.state().pipeline); + render_pass.setBindGroup(0, binding0, null); + render_pass.setBindGroup(1, binding1, null); + render_pass.draw(18, 1, 0, 0); + + // Finish render pass + render_pass.end(); + + // Submit our commands to the queue + var command = encoder.finish(&.{ .label = label }); + defer command.release(); + core.state().queue.submit(&[_]*gpu.CommandBuffer{command}); + + // Present the frame + core.schedule(.present_frame); + + // update the window title every second + if (game.state().title_timer.read() >= 1.0) { + game.state().title_timer.reset(); + try updateWindowTitle(core); + } +} + +fn updateWindowTitle(core: *mach.Core.Mod) !void { + try mach.Core.printTitle( + core, + core.state().main_window, + "core-custom-entrypoint [ {d}fps ] [ Input {d}hz ]", + .{ + // TODO(Core) + mach.core.frameRate(), + mach.core.inputRate(), + }, + ); + core.schedule(.update); +} diff --git a/src/views/shader.wgsl b/src/views/shader.wgsl new file mode 100644 index 0000000..bea44a7 --- /dev/null +++ b/src/views/shader.wgsl @@ -0,0 +1,91 @@ +struct Config { + seg1: f32, + seg2: f32, +}; + +@group(0) @binding(0) var mainSampler: sampler; +@group(0) @binding(1) var mainTexture: texture_2d; +@group(0) @binding(2) var config: Config; + +struct Result { + rotation: f32, +}; + +@group(1) @binding(0) var result: Result; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) texcoord: vec2, +}; + +@vertex fn vertex_main( + @builtin(vertex_index) VertexIndex : u32 +) -> VertexOutput { + var uvSeg1 = config.seg1 * 2 - 1; + var uvSeg2 = config.seg2 * 2 - 1; + + var rotation = result.rotation; + + var pos = array, 18>( + // Seg 1 + vec2(-1.0, -1.0), // ↙ + vec2(1.0, -1.0), // ↘ + vec2(-1.0, uvSeg1), // ↖ + vec2(-1.0, uvSeg1), // ↖ + vec2(1.0, -1.0), // ↘ + vec2(1.0, uvSeg1), // ↗ + + // Seg 2 + vec2(-1.0, uvSeg1), // ↙ + vec2(1.0, uvSeg1), // ↘ + vec2(-1.0, uvSeg2), // ↖ + vec2(-1.0, uvSeg2), // ↖ + vec2(1.0, uvSeg1), // ↘ + vec2(1.0, uvSeg2), // ↗ + + // Seg 3 + vec2(-1.0, uvSeg2), // ↙ + vec2(1.0, uvSeg2), // ↘ + vec2(-1.0, 1.0), // ↖ + vec2(-1.0, 1.0), // ↖ + vec2(1.0, uvSeg2), // ↘ + vec2(1.0, 1.0), // ↗ + ); + + var vsOutput: VertexOutput; + let xy = pos[VertexIndex]; + + vsOutput.texcoord = xy;//flipY(xy); + + if (VertexIndex < 8 || VertexIndex == 10) { + vsOutput.position = vec4(xy, 0.0, 1.0); + } else { + vsOutput.position = vec4(rotate(xy, uvSeg1 + 0.2, rotation), 0.0, 1.0); + }; + return vsOutput; +} + +fn rotate(xy: vec2, y: f32, r: f32) -> vec2{ + var angle = atan2(xy.y - y, xy.x); + var l = length(vec2(xy.x, xy.y - y)); + return vec2(l * cos(angle - r), l * sin(angle - r) + y); +} + +fn flipY(xy: vec2) -> vec2 { + return xy * vec2(0.5, -0.5) + 0.5; +} + +// fn mapping(pos: vec2) -> vec2 { +// let modulus: f32 = length(pos); +// let rotation: f32 = atan2(pos.y, pos.x) + pow(modulus / 1.414 - 1, 3) * 2; +// return vec2(cos(rotation), sin(rotation)) * modulus; +// } +fn mapping(pos: vec2) -> vec2 { + let modulus: f32 = length(pos); + let rotation: f32 = atan2(pos.y, pos.x) + pow(modulus / 1.414 - 1, 3) * 2; + return vec2(cos(rotation), sin(rotation)) * modulus; +} + +@fragment fn frag_main(fsInput: VertexOutput) -> @location(0) vec4 { + return textureSample(mainTexture, mainSampler, flipY(fsInput.texcoord)); +}