commit d23c93466286e9e230a23fb5a7b1af41a07bcefe Author: SamratGhale Date: Tue Mar 10 22:11:28 2026 +0545 Init diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d1604b --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Edit2d + +Edit2d is my attempt to make a box2d editor, where i can create, simulate, edit and save them using a single tool +My main goal is to create game using it +It's created using folloing tools + +1. Odin programming language +2. GLFW for input handling and graphics +3. Dear ImGui for user interfaces +4. Box2d + +## Current Progress + +Currently it can create, edit and save all kinds of bodies and shape + +## Working on + +Create edit and save different kinds of joints + diff --git a/draw.odin b/draw.odin new file mode 100644 index 0000000..b141283 --- /dev/null +++ b/draw.odin @@ -0,0 +1,1645 @@ +package ion + +import "core:strings" +import "base:runtime" +import "core:fmt" +import "core:math" +import "core:math/linalg" +import im "shared:odin-imgui" +import gl "vendor:OpenGL" +import b2 "vendor:box2d" +import "vendor:glfw" + + +Camera :: struct { + center : b2.Vec2, + width, height : i32, + zoom : f32, + rotation : f32, +} + +RGBA8 :: [4]u8 + +make_rgba :: proc(color : b2.HexColor, alpha : f32) -> RGBA8 { + c := i32(color) + return { + u8((c >> 16) & 0xFF), + u8((c >> 8) & 0xFF), + u8(c & 0xFF), + u8(0xFF * alpha), + } +} + + +camera_reset_view :: proc(camera : ^Camera) { + camera.center = {0, 0} + camera.zoom = 1 +} + + +camera_init :: proc() -> Camera { + c : Camera = { + width = 1920, + height = 1080, + } + camera_reset_view(&c) + return c +} + +//Takes in a vector that is screen's pixel coordinate and converts it to world's coordinate (according to the camera) +camera_convert_screen_to_world_64 :: proc( + cam : ^Camera, + ps : [2]f64, +) -> b2.Vec2 { + + ps_32 :[2]f32= {f32(ps.x), f32(ps.y)} + + return camera_convert_screen_to_world_32(cam, ps_32) + +} + +camera_convert_screen_to_world_32 :: proc( + cam : ^Camera, + ps : b2.Vec2, +) -> b2.Vec2 { + + ps :[2]f32= {f32(ps.x), f32(ps.y)} + + w := f32(cam.width) + h := f32(cam.height) + u := ps.x / w + v := (h - ps.y) / h + + ratio := w / h + extents : b2.Vec2 = {cam.zoom * ratio, cam.zoom} + + lower := cam.center - extents + upper := cam.center + extents + + pw : b2.Vec2 = { + (1.0 - u) * lower.x + u * upper.x, + (1.0 - v) * lower.y + v * upper.y, + } + return pw +} + +camera_convert_screen_to_world :: proc { + camera_convert_screen_to_world_32, + camera_convert_screen_to_world_64 +} + + +camera_convert_world_to_screen :: proc( + cam : ^Camera, + pw : b2.Vec2, +) -> b2.Vec2 { + cam := cam + pw := pw + + w := f32(cam.width) + h := f32(cam.height) + + switch cam.rotation { + case 1 ..= 90, 180 ..= 270: + pw = swizzle(pw, 1, 0) + pw.y = -pw.y + case 180: + pw.y = -pw.y + } + ratio := w / h + + extents : b2.Vec2 = {cam.zoom * ratio, cam.zoom} + + + rotated_pw := pw + lower := cam.center - extents + upper := cam.center + extents + + + u := (rotated_pw.x - lower.x) / (upper.x - lower.x) + v := (rotated_pw.y - lower.y) / (upper.y - lower.y) + + ps : b2.Vec2 = {u * w, (1.0 - v) * h} + return ps +} + +PI :: 3.14159265358979323846 +DEG2RAD :: PI / 180.0 +RAD2DEG :: 180.0 / PI + + +//Convert from world coordinates to normalized device coordinates +// http://www.songho.ca/opengl/gl_projectionmatrix.html +camera_build_project_matrix :: proc( + cam : ^Camera, + z_bias : f32, +) -> matrix[4, 4]f32 { + + m : matrix[4, 4]f32 + + mat_rot := linalg.matrix4_rotate_f32(DEG2RAD * cam.rotation, {0, 0, 1}) + + ratio := f32(cam.width) / f32(cam.height) + extents : b2.Vec2 = {cam.zoom * ratio, cam.zoom} + lower := cam.center - extents + upper := cam.center + extents + + w := upper.x - lower.x + h := upper.y - lower.y + + m[0][0] = 2.0 / w + m[1][1] = 2.0 / h + m[2][2] = -1 + m[3][0] = -2.0 * cam.center.x / w + m[3][1] = -2.0 * cam.center.y / h + m[3][2] = z_bias + m[3][3] = 1 + + return m * mat_rot +} + +camera_get_view_bounds :: proc(cam : ^Camera) -> b2.AABB { + return b2.AABB { + lowerBound = camera_convert_screen_to_world(cam, b2.Vec2{0, f32(cam.height)}), + upperBound = camera_convert_screen_to_world(cam, b2.Vec2{f32(cam.width), 0}), + } +} + + +Background :: struct { + vao, vbo, program : u32, + uniforms : gl.Uniforms, +} + +check_opengl :: proc() { + err := gl.GetError() + if err != gl.NO_ERROR { + fmt.eprintf("OpenGL error = %d\n", err) + assert(false) + } +} + + +background_create :: proc(back : ^Background) { + + ok : bool + back.program, ok = gl.load_shaders_source( + #load("shaders/background.vs"), + #load("shaders/background.fs"), + ) + check_opengl() + back.uniforms = gl.get_uniforms_from_program(back.program) + + vertex_attribute : u32 + + //Generate + gl.GenVertexArrays(1, &back.vao) + gl.GenBuffers(1, &back.vbo) + + gl.BindVertexArray(back.vao) + gl.EnableVertexAttribArray(vertex_attribute) + + //Single quad + vertices : [4]b2.Vec2 = {{-1.0, 1.0}, {-1.0, -1.0}, {1.0, 1.0}, {1.0, -1}} + gl.BindBuffer(gl.ARRAY_BUFFER, back.vbo) + gl.BufferData( + gl.ARRAY_BUFFER, + size_of(vertices), + &vertices[0], + gl.STATIC_DRAW, + ) + gl.VertexAttribPointer(vertex_attribute, 2, gl.FLOAT, false, 0, 0) + + check_opengl() + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) +} + +background_destroy :: proc(back : ^Background) { + if bool(back.vao) { + gl.DeleteVertexArrays(1, &back.vao) + gl.DeleteBuffers(1, &back.vbo) + back.vao = 0 + back.vbo = 0 + } + + if bool(back.program) { + gl.DeleteProgram(back.program) + back.program = 0 + } +} + +background_draw :: proc(back : ^Background, cam : ^Camera) { + gl.UseProgram(back.program) + + time := f32(glfw.GetTime()) + time = math.mod_f32(time, f32(10.0)) + + gl.Uniform1f(back.uniforms["time"].location, time) + gl.Uniform2f( + back.uniforms["resolution"].location, + f32(cam.width), + f32(cam.height), + ) + + gl.Uniform3f(back.uniforms["baseColor"].location, 0.4, 0.4, 0.2) + + gl.BindVertexArray(back.vao) + gl.BindBuffer(gl.ARRAY_BUFFER, back.vbo) + gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + gl.UseProgram(0) +} + + +PointData :: struct { + pos : b2.Vec2, + size : f32, + rgba : RGBA8, +} + +Point :: struct { + vao, vbo, program : u32, + uniforms : gl.Uniforms, + points : [dynamic]PointData, +} + +points_create :: proc(point : ^Point) { + + vs : string = ` + #version 330 + uniform mat4 projectionMatrix; + layout(location = 0) in vec2 v_position; + layout(location = 1) in float v_size; + layout(location = 2) in vec4 v_color; + out vec4 f_color; + void main(void) { + f_color = v_color; + gl_Position = projectionMatrix * vec4(v_position, 0.0f, 1.0f); + gl_PointSize = v_size; + } + ` + + fs : string = ` + #version 330 + in vec4 f_color; + out vec4 color; + void main(void){ + color = f_color; + } + ` + point.program, _ = gl.load_shaders_source(vs, fs) + point.uniforms = gl.get_uniforms_from_program(point.program) + + vertex_attribute : u32 = 0 + size_attribute : u32 = 1 + color_attribute : u32 = 2 + + gl.GenVertexArrays(1, &point.vao) + gl.GenBuffers(1, &point.vbo) + + gl.BindVertexArray(point.vao) + gl.EnableVertexAttribArray(vertex_attribute) + gl.EnableVertexAttribArray(size_attribute) + gl.EnableVertexAttribArray(color_attribute) + + //Vertex buffer + gl.BindBuffer(gl.ARRAY_BUFFER, point.vbo) + gl.BufferData( + gl.ARRAY_BUFFER, + 2048 * size_of(PointData), + nil, + gl.DYNAMIC_DRAW, + ) + + gl.VertexAttribPointer( + vertex_attribute, + 2, + gl.FLOAT, + gl.FALSE, + size_of(PointData), + offset_of(PointData, pos), + ) + + gl.VertexAttribPointer( + size_attribute, + 1, + gl.FLOAT, + gl.FALSE, + size_of(PointData), + offset_of(PointData, size), + ) + + gl.VertexAttribPointer( + color_attribute, + 4, + gl.UNSIGNED_BYTE, + gl.TRUE, + size_of(PointData), + offset_of(PointData, rgba), + ) + + check_opengl() + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + +} + +points_destroy :: proc(point : ^Point) { + if point.vao != 0 { + gl.DeleteVertexArrays(1, &point.vao) + gl.DeleteBuffers(1, &point.vbo) + point.vao = 0 + point.vbo = 0 + } + + if point.program != 0 { + gl.DeleteProgram(point.program) + point.program = 0 + } +} + +points_add :: proc(point : ^Point, v : b2.Vec2, size : f32, c : b2.HexColor) { + rgba := make_rgba(c, 1.0) + append(&point.points, PointData{v, size, rgba}) +} + +//Flush means draw +points_flush :: proc(point : ^Point, cam : ^Camera) { + count := i32(len(point.points)) + if count == 0 do return + + gl.UseProgram(point.program) + + proj := camera_build_project_matrix(cam, 0) + + gl.UniformMatrix4fv( + point.uniforms["projectionMatrix"].location, + 1, + gl.FALSE, + &proj[0][0], + ) + + gl.BindVertexArray(point.vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, point.vbo) + gl.Enable(gl.PROGRAM_POINT_SIZE) + + base := 0 + + for count > 0 { + batch_count : i32 = min(count, 2048) + + gl.BufferSubData( + gl.ARRAY_BUFFER, + 0, + int(batch_count * size_of(PointData)), + &point.points[base], + ) + gl.DrawArrays(gl.POINTS, 0, batch_count) + + check_opengl() + + count -= 2048 + base += 2048 + } + + gl.Disable(gl.PROGRAM_POINT_SIZE) + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + gl.UseProgram(0) + + clear(&point.points) +} + +VertexData :: struct { + pos : b2.Vec2, + rgba : RGBA8, +} + +Lines :: struct { + points : [dynamic]VertexData, + vao, vbo, program : u32, + uniforms : gl.Uniforms, +} + + +lines_create :: proc(line : ^Lines) { + + vs : string = ` + #version 330 + uniform mat4 projectionMatrix; + layout(location = 0) in vec2 v_position; + layout(location = 1) in vec4 v_color; + out vec4 f_color; + + void main(void){ + f_color = v_color; + gl_Position = projectionMatrix * vec4(v_position , 0.0f, 1.0f); + } + ` + + fs : string = ` + #version 330 + in vec4 f_color; + out vec4 color; + + void main(void){ + color = f_color; + } + ` + + ok := false + line.program, ok = gl.load_shaders_source(vs, fs) + check_opengl() + line.uniforms = gl.get_uniforms_from_program(line.program) + + vertex_attribute : u32 = 0 + color_attribute : u32 = 1 + + gl.GenVertexArrays(1, &line.vao) + gl.GenBuffers(1, &line.vbo) + + gl.BindVertexArray(line.vao) + gl.EnableVertexAttribArray(vertex_attribute) + gl.EnableVertexAttribArray(color_attribute) + + //Vertex buffer + gl.BindBuffer(gl.ARRAY_BUFFER, line.vbo) + gl.BufferData( + gl.ARRAY_BUFFER, + 2048 * size_of(VertexData), + nil, + gl.DYNAMIC_DRAW, + ) + + gl.VertexAttribPointer( + vertex_attribute, + 2, + gl.FLOAT, + gl.FALSE, + size_of(VertexData), + offset_of(VertexData, pos), + ) + gl.VertexAttribPointer( + color_attribute, + 4, + gl.UNSIGNED_BYTE, + gl.TRUE, + size_of(VertexData), + offset_of(VertexData, rgba), + ) + check_opengl() + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) +} + +lines_destroy :: proc(line : ^Lines) { + if line.vao != 0 { + gl.DeleteVertexArrays(1, &line.vao) + gl.DeleteBuffers(1, &line.vbo) + line.vao = 0 + line.vbo = 0 + } + + if line.program != 0 { + gl.DeleteProgram(line.program) + line.program = 0 + } +} + + +lines_add :: proc(line : ^Lines, p1, p2 : b2.Vec2, c : b2.HexColor) { + rgba := make_rgba(c, 1.0) + append(&line.points, VertexData{p1, rgba}) + append(&line.points, VertexData{p2, rgba}) +} + + +lines_flush :: proc(line : ^Lines, cam : ^Camera) { + count := i32(len(line.points)) + + batch_size : i32 = 2 * 2048 + + if count == 0 do return + + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + + gl.UseProgram(line.program) + + proj := camera_build_project_matrix(cam, 0.1) + + gl.UniformMatrix4fv( + line.uniforms["projectionMatrix"].location, + 1, + gl.FALSE, + &proj[0][0], + ) + gl.BindVertexArray(line.vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, line.vbo) + + base : i32 = 0 + + for count > 0 { + batch_count := min(count, batch_size) + size := int(batch_count * size_of(VertexData)) + gl.BufferSubData(gl.ARRAY_BUFFER, 0, size, &line.points[base]) + gl.DrawArrays(gl.LINES, 0, batch_count) + check_opengl() + + count -= batch_size + base += batch_size + } + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + gl.UseProgram(0) + gl.Disable(gl.BLEND) + clear(&line.points) +} + + +CircleData :: struct { + pos : b2.Vec2, + radius : f32, + rgba : RGBA8, +} + +Circles :: struct { + circles : [dynamic]CircleData, + vao, program : u32, + vbos : [2]u32, + uniforms : gl.Uniforms, +} + + +circle_create :: proc(circle : ^Circles) { + + batch_size := 2048 + + circle.program, _ = gl.load_shaders_source( + #load("shaders/circle.vs"), + #load("shaders/circle.fs"), + ) + check_opengl() + circle.uniforms = gl.get_uniforms_from_program(circle.program) + + vertex_attribute : u32 = 0 + position_instance : u32 = 1 + radiusInstance : u32 = 2 + colorInstance : u32 = 3 + + gl.GenVertexArrays(1, &circle.vao) + gl.GenBuffers(2, &circle.vbos[0]) + + gl.BindVertexArray(circle.vao) + gl.EnableVertexAttribArray(vertex_attribute) + gl.EnableVertexAttribArray(position_instance) + gl.EnableVertexAttribArray(radiusInstance) + gl.EnableVertexAttribArray(colorInstance) + + //vertex buffer for single quad + a : f32 = 1.1 + + vertices : []b2.Vec2 = { + {-a, -a}, + {a, -a}, + {-a, a}, + {a, -a}, + {a, a}, + {-a, a}, + } + + gl.BindBuffer(gl.ARRAY_BUFFER, circle.vbos[0]) + gl.BufferData( + gl.ARRAY_BUFFER, + size_of(b2.Vec2) * 6, + &vertices[0], + gl.STATIC_DRAW, + ) + gl.VertexAttribPointer(vertex_attribute, 2, gl.FLOAT, gl.FALSE, 0, 0) + + // + gl.BindBuffer(gl.ARRAY_BUFFER, circle.vbos[1]) + gl.BufferData( + gl.ARRAY_BUFFER, + batch_size * size_of(CircleData), + nil, + gl.DYNAMIC_DRAW, + ) + + gl.VertexAttribPointer( + position_instance, + 2, + gl.FLOAT, + gl.FALSE, + size_of(CircleData), + offset_of(CircleData, pos), + ) + gl.VertexAttribPointer( + radiusInstance, + 1, + gl.FLOAT, + gl.FALSE, + size_of(CircleData), + offset_of(CircleData, radius), + ) + gl.VertexAttribPointer( + colorInstance, + 4, + gl.UNSIGNED_BYTE, + gl.TRUE, + size_of(CircleData), + offset_of(CircleData, rgba), + ) + + gl.VertexAttribDivisor(position_instance, 1) + gl.VertexAttribDivisor(radiusInstance, 1) + gl.VertexAttribDivisor(colorInstance, 1) + + check_opengl() + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) +} + +circle_destroy :: proc(circle : ^Circles) { + if circle.vao != 0 { + gl.DeleteVertexArrays(1, &circle.vao) + gl.DeleteBuffers(2, &circle.vbos[0]) + circle.vao = 0 + circle.vbos[0] = 0 + circle.vbos[1] = 0 + } + + if circle.program != 0 { + gl.DeleteProgram(circle.program) + circle.program = 0 + } +} + +circle_add :: proc( + circle : ^Circles, + center : b2.Vec2, + radius : f32, + color : b2.HexColor, +) { + rgba := make_rgba(color, 1.0) + append(&circle.circles, CircleData{center, radius, rgba}) +} + +circle_flush :: proc(circle : ^Circles, cam : ^Camera) { + count := i32(len(circle.circles)) + if count == 0 do return + batch_size : i32 = 2048 + + gl.UseProgram(circle.program) + + proj := camera_build_project_matrix(cam, 0.2) + + gl.UniformMatrix4fv( + circle.uniforms["projectionMatrix"].location, + 1, + gl.FALSE, + &proj[0][0], + ) + gl.Uniform1f( + circle.uniforms["pixelScale"].location, + f32(cam.height) / cam.zoom, + ) + + gl.BindVertexArray(circle.vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, circle.vbos[1]) + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + + base : i32 = 0 + + for count > 0 { + batch_count := min(count, batch_size) + + gl.BufferSubData( + gl.ARRAY_BUFFER, + 0, + int(batch_count * size_of(CircleData)), + &circle.circles[base], + ) + gl.DrawArraysInstanced(gl.TRIANGLES, 0, 6, batch_count) + + check_opengl() + + count -= batch_size + base += batch_size + } + + gl.Disable(gl.BLEND) + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + gl.UseProgram(0) + + clear(&circle.circles) +} + + +SolidCircleData :: struct { + transform : b2.Transform, + radius : f32, + rgba : RGBA8, +} + +SolidCircle :: struct { + circles : [dynamic]SolidCircleData, + program, vao : u32, + vbo : [2]u32, + uniforms : gl.Uniforms, +} + + +solid_circle_create :: proc(circle : ^SolidCircle) { + circle.program, _ = gl.load_shaders_source( + #load("shaders/solid_circle.vs"), + #load("shaders/solid_circle.fs"), + ) + circle.uniforms = gl.get_uniforms_from_program(circle.program) + + gl.GenVertexArrays(1, &circle.vao) + gl.GenBuffers(2, &circle.vbo[0]) + + gl.BindVertexArray(circle.vao) + + vertex_attribute : u32 = 0 + transform_instance : u32 = 1 + radius_instance : u32 = 2 + color_instance : u32 = 3 + + gl.EnableVertexAttribArray(vertex_attribute) + gl.EnableVertexAttribArray(transform_instance) + gl.EnableVertexAttribArray(radius_instance) + gl.EnableVertexAttribArray(color_instance) + + batch_size : i32 = 2048 + + //Vertex buffer for single quad + a : f32 = 1.1 + + vertices : []b2.Vec2 = { + {-a, -a}, + {a, -a}, + {-a, a}, + {a, -a}, + {a, a}, + {-a, a}, + } + + gl.BindBuffer(gl.ARRAY_BUFFER, circle.vbo[0]) + gl.BufferData( + gl.ARRAY_BUFFER, + size_of(b2.Vec2) * 6, + &vertices[0], + gl.STATIC_DRAW, + ) + gl.VertexAttribPointer(vertex_attribute, 2, gl.FLOAT, gl.FALSE, 0, 0) + + // + gl.BindBuffer(gl.ARRAY_BUFFER, circle.vbo[1]) + gl.BufferData( + gl.ARRAY_BUFFER, + int(batch_size * size_of(SolidCircleData)), + nil, + gl.DYNAMIC_DRAW, + ) + + gl.VertexAttribPointer( + transform_instance, + 4, + gl.FLOAT, + gl.FALSE, + size_of(SolidCircleData), + offset_of(SolidCircleData, transform), + ) + gl.VertexAttribPointer( + radius_instance, + 1, + gl.FLOAT, + gl.FALSE, + size_of(SolidCircleData), + offset_of(SolidCircleData, radius), + ) + gl.VertexAttribPointer( + color_instance, + 4, + gl.UNSIGNED_BYTE, + gl.TRUE, + size_of(SolidCircleData), + offset_of(SolidCircleData, rgba), + ) + + gl.VertexAttribDivisor(transform_instance, 1) + gl.VertexAttribDivisor(radius_instance, 1) + gl.VertexAttribDivisor(color_instance, 1) + + check_opengl() + + //Cleanup + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) +} + +solid_circle_destroy :: proc(circle : ^SolidCircle) { + if circle.vao != 0 { + gl.DeleteVertexArrays(1, &circle.vao) + gl.DeleteBuffers(2, &circle.vbo[0]) + circle.vao = 0 + circle.vbo[0] = 0 + circle.vbo[1] = 0 + } + + if circle.program != 0 { + gl.DeleteProgram(circle.program) + circle.program = 0 + } +} + +solid_circle_add :: proc( + circle : ^SolidCircle, + transform : b2.Transform, + radius : f32, + color : b2.HexColor, +) { + rgba := make_rgba(color, 1.0) + append(&circle.circles, SolidCircleData{transform, radius, rgba}) +} + +solid_circle_flush :: proc(circle : ^SolidCircle, cam : ^Camera) { + count : i32 = i32(len(circle.circles)) + if count == 0 do return + batch_size : i32 = 2048 + + gl.UseProgram(circle.program) + + proj := camera_build_project_matrix(cam, 0.2) + + gl.UniformMatrix4fv( + circle.uniforms["projectionMatrix"].location, + 1, + gl.FALSE, + &proj[0][0], + ) + gl.Uniform1f( + circle.uniforms["pixelScale"].location, + f32(cam.height) / cam.zoom, + ) + + gl.BindVertexArray(circle.vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, circle.vbo[1]) + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + + base : i32 = 0 + + for count > 0 { + batch_count := min(count, batch_size) + + gl.BufferSubData( + gl.ARRAY_BUFFER, + 0, + int(batch_count * size_of(SolidCircleData)), + &circle.circles[base], + ) + gl.DrawArraysInstanced(gl.TRIANGLES, 0, 6, batch_count) + + check_opengl() + + count -= batch_size + base += batch_size + } + + gl.Disable(gl.BLEND) + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + gl.UseProgram(0) + + clear(&circle.circles) +} + + +CapsuleData :: struct { + transform : b2.Transform, + radius, length : f32, + rgba : RGBA8, +} + + +SolidCapsules :: struct { + capsules : [dynamic]CapsuleData, + vao, program : u32, + vbo : [2]u32, + uniforms : gl.Uniforms, +} + + +//Draw capsules using SDF-based shaders + +solid_capsules_create :: proc(capsule : ^SolidCapsules) { + capsule.program, _ = gl.load_shaders_source( + #load("shaders/solid_capsule.vs"), + #load("shaders/solid_capsule.fs"), + ) + check_opengl() + capsule.uniforms = gl.get_uniforms_from_program(capsule.program) + + + //batch_size := i32(len(capsules)) + batch_size : i32 = 512 + + + vertex_attribute : u32 = 0 + transform_instance : u32 = 1 + radius_instance : u32 = 2 + length_instance : u32 = 3 + color_instance : u32 = 4 + + gl.GenVertexArrays(1, &capsule.vao) + gl.GenBuffers(2, &capsule.vbo[0]) + + gl.BindVertexArray(capsule.vao) + + gl.EnableVertexAttribArray(vertex_attribute) + gl.EnableVertexAttribArray(transform_instance) + gl.EnableVertexAttribArray(radius_instance) + gl.EnableVertexAttribArray(length_instance) + gl.EnableVertexAttribArray(color_instance) + + //Vertex buffer for single quad + a : f32 = 1.1 + + vertices : []b2.Vec2 = { + {-a, -a}, + {a, -a}, + {-a, a}, + {a, -a}, + {a, a}, + {-a, a}, + } + + gl.BindBuffer(gl.ARRAY_BUFFER, capsule.vbo[0]) + gl.BufferData( + gl.ARRAY_BUFFER, + size_of(b2.Vec2) * 6, + &vertices[0], + gl.STATIC_DRAW, + ) + gl.VertexAttribPointer(vertex_attribute, 2, gl.FLOAT, gl.FALSE, 0, 0) + + // + gl.BindBuffer(gl.ARRAY_BUFFER, capsule.vbo[1]) + gl.BufferData( + gl.ARRAY_BUFFER, + int(batch_size * size_of(CapsuleData)), + nil, + gl.DYNAMIC_DRAW, + ) + + gl.VertexAttribPointer( + transform_instance, + 4, + gl.FLOAT, + gl.FALSE, + size_of(CapsuleData), + offset_of(CapsuleData, transform), + ) + gl.VertexAttribPointer( + radius_instance, + 1, + gl.FLOAT, + gl.FALSE, + size_of(CapsuleData), + offset_of(CapsuleData, radius), + ) + gl.VertexAttribPointer( + length_instance, + 1, + gl.FLOAT, + gl.FALSE, + size_of(CapsuleData), + offset_of(CapsuleData, length), + ) + gl.VertexAttribPointer( + color_instance, + 4, + gl.UNSIGNED_BYTE, + gl.TRUE, + size_of(CapsuleData), + offset_of(CapsuleData, rgba), + ) + + gl.VertexAttribDivisor(transform_instance, 1) + gl.VertexAttribDivisor(radius_instance, 1) + gl.VertexAttribDivisor(length_instance, 1) + gl.VertexAttribDivisor(color_instance, 1) + + check_opengl() + + //Cleanup + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) +} + +solid_capsules_destroy :: proc(capsule : ^SolidCapsules) { + if capsule.vao != 0 { + gl.DeleteVertexArrays(1, &capsule.vao) + gl.DeleteBuffers(2, &capsule.vbo[0]) + capsule.vao = 0 + capsule.vbo = {0, 0} + } + + if capsule.program != 0 { + gl.DeleteProgram(capsule.program) + capsule.program = 0 + } +} + +solid_capsules_add :: proc( + capsule : ^SolidCapsules, + p1, p2 : b2.Vec2, + radius : f32, + c : b2.HexColor, +) { + d := p2 - p1 + + length := b2.Length(d) + if length < 0.001 do return + + axis := d / length + + transform : b2.Transform = { + p = 0.5 * (p1 + p2), + q = {c = axis.x, s = axis.y}, + } + + rgba := make_rgba(c, 1.0) + + append(&capsule.capsules, CapsuleData{transform, radius, length, rgba}) +} + +solid_capsules_flush :: proc(capsule : ^SolidCapsules, cam : ^Camera) { + count := i32(len(capsule.capsules)) + + if count == 0 do return + + //batch :i32= 2048 + + gl.UseProgram(capsule.program) + + proj := camera_build_project_matrix(cam, 0.2) + + gl.UniformMatrix4fv( + capsule.uniforms["projectionMatrix"].location, + 1, + gl.FALSE, + &proj[0][0], + ) + gl.Uniform1f( + capsule.uniforms["pixelScale"].location, + f32(cam.height) / cam.zoom, + ) + + gl.BindVertexArray(capsule.vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, capsule.vbo[1]) + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + + base : i32 = 0 + + for count > 0 { + batch_count := min(count, 2048) + //fmt.println(size_of(capsules[0])) + + gl.BufferSubData( + gl.ARRAY_BUFFER, + 0, + int(batch_count * size_of(CapsuleData)), + &capsule.capsules[base], + ) + gl.DrawArraysInstanced(gl.TRIANGLES, 0, 6, batch_count) + + check_opengl() + + count -= 2048 + base += 2048 + } + + gl.Disable(gl.BLEND) + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + gl.UseProgram(0) + + clear(&capsule.capsules) +} + + +PolygonData :: struct #packed { + transform : b2.Transform, + p1, p2, p3, p4, p5, p6, p7, p8 : b2.Vec2, + count : i32, + radius : f32, + + //Keep color small + color : RGBA8, +} + +SolidPolygon :: struct { + polygons : [dynamic]PolygonData, + vao, program : u32, + vbo : [2]u32, + uniforms : gl.Uniforms, +} + + +solid_polygon_create :: proc(polygon : ^SolidPolygon) { + polygon.program, _ = gl.load_shaders_source( + #load("shaders/solid_polygons.vs"), + #load("shaders/solid_polygons.fs"), + ) + + batch_size : i32 = 512 + + polygon.uniforms = gl.get_uniforms_from_program(polygon.program) + + vertex_attribute : u32 = 0 + instance_transform : u32 = 1 + instance_point12 : u32 = 2 + instance_point34 : u32 = 3 + instance_point56 : u32 = 4 + instance_point78 : u32 = 5 + instance_point_count : u32 = 6 + instance_radius : u32 = 7 + instance_color : u32 = 8 + + + gl.GenVertexArrays(1, &polygon.vao) + gl.GenBuffers(2, &polygon.vbo[0]) + + gl.BindVertexArray(polygon.vao) + + gl.EnableVertexAttribArray(vertex_attribute) + gl.EnableVertexAttribArray(instance_transform) + gl.EnableVertexAttribArray(instance_point12) + gl.EnableVertexAttribArray(instance_point34) + gl.EnableVertexAttribArray(instance_point56) + gl.EnableVertexAttribArray(instance_point78) + gl.EnableVertexAttribArray(instance_point_count) + gl.EnableVertexAttribArray(instance_radius) + gl.EnableVertexAttribArray(instance_color) + + a : f32 = 1.1 + + vertices : []b2.Vec2 = { + {-a, -a}, + {a, -a}, + {-a, a}, + {a, -a}, + {a, a}, + {-a, a}, + } + + gl.BindBuffer(gl.ARRAY_BUFFER, polygon.vbo[0]) + gl.BufferData( + gl.ARRAY_BUFFER, + size_of(b2.Vec2) * 6, + &vertices[0], + gl.STATIC_DRAW, + ) + gl.VertexAttribPointer(vertex_attribute, 2, gl.FLOAT, gl.FALSE, 0, 0) + + // + gl.BindBuffer(gl.ARRAY_BUFFER, polygon.vbo[1]) + gl.BufferData( + gl.ARRAY_BUFFER, + int(batch_size * size_of(PolygonData)), + nil, + gl.DYNAMIC_DRAW, + ) + + gl.VertexAttribPointer( + instance_transform, + 4, + gl.FLOAT, + false, + size_of(PolygonData), + offset_of(PolygonData, transform), + ) + gl.VertexAttribPointer( + instance_point12, + 4, + gl.FLOAT, + false, + size_of(PolygonData), + offset_of(PolygonData, p1), + ) + gl.VertexAttribPointer( + instance_point34, + 4, + gl.FLOAT, + false, + size_of(PolygonData), + offset_of(PolygonData, p3), + ) + gl.VertexAttribPointer( + instance_point56, + 4, + gl.FLOAT, + false, + size_of(PolygonData), + offset_of(PolygonData, p5), + ) + gl.VertexAttribPointer( + instance_point78, + 4, + gl.FLOAT, + false, + size_of(PolygonData), + offset_of(PolygonData, p7), + ) + gl.VertexAttribIPointer( + instance_point_count, + 1, + gl.INT, + size_of(PolygonData), + offset_of(PolygonData, count), + ) + gl.VertexAttribPointer( + instance_radius, + 1, + gl.FLOAT, + false, + size_of(PolygonData), + offset_of(PolygonData, radius), + ) + gl.VertexAttribPointer( + instance_color, + 4, + gl.UNSIGNED_BYTE, + true, + size_of(PolygonData), + offset_of(PolygonData, color), + ) + + + gl.VertexAttribDivisor(instance_transform, 1) + gl.VertexAttribDivisor(instance_point12, 1) + gl.VertexAttribDivisor(instance_point34, 1) + gl.VertexAttribDivisor(instance_point56, 1) + gl.VertexAttribDivisor(instance_point78, 1) + gl.VertexAttribDivisor(instance_point_count, 1) + gl.VertexAttribDivisor(instance_radius, 1) + gl.VertexAttribDivisor(instance_color, 1) + + check_opengl() + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + +} + +solid_polygon_add :: proc( + polygon : ^SolidPolygon, + transform : b2.Transform, + points : [^]b2.Vec2, + count : i32, + radius : f32, + color : b2.HexColor, +) { + + data : PolygonData + + data.transform = transform + + n := min(count, 8) + + ps := cast([^]b2.Vec2)&data.p1 + + for i in 0 ..< count { + //fmt.print(points[i]) + ps[i] = points[i] + } + + data.count = n + data.radius = f32(radius) + data.color = make_rgba(color, 1.0) + + append(&polygon.polygons, data) +} + + +solid_polygon_flush :: proc(polygon : ^SolidPolygon, cam : ^Camera) { + count := i32(len(polygon.polygons)) + + if count == 0 do return + + batch_size : i32 = 512 + + gl.UseProgram(polygon.program) + + proj := camera_build_project_matrix(cam, 0.2) + + gl.UniformMatrix4fv( + polygon.uniforms["projectionMatrix"].location, + 1, + gl.FALSE, + &proj[0][0], + ) + gl.Uniform1f( + polygon.uniforms["pixelScale"].location, + f32(cam.height) / cam.zoom, + ) + + gl.BindVertexArray(polygon.vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, polygon.vbo[1]) + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + + base : i32 = 0 + + for count > 0 { + batch_count := min(count, batch_size) + + gl.BufferSubData( + gl.ARRAY_BUFFER, + 0, + int(batch_count * size_of(PolygonData)), + &polygon.polygons[base], + ) + gl.DrawArraysInstanced(gl.TRIANGLES, 0, 6, batch_count) + + check_opengl() + + count -= batch_size + base += batch_size + } + + gl.Disable(gl.BLEND) + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + gl.UseProgram(0) + + clear(&polygon.polygons) + +} + + +Draw :: struct { + show_ui : bool, + debug_draw : b2.DebugDraw, + cam : Camera, + background : Background, + points : Point, + lines : Lines, + circles : Circles, + solid_circles : SolidCircle, + solid_capsules : SolidCapsules, + polygons : SolidPolygon, + drawCounters : bool, + regular_font : im.Font, + frame_buffer : u32, +} + +draw_aabb :: proc(draw : ^Draw, aabb : b2.AABB, c : b2.HexColor) { + p1 := aabb.lowerBound + p2 : [2]f32 = {aabb.upperBound.x, aabb.lowerBound.y} + + p3 := aabb.upperBound + p4 : [2]f32 = {aabb.lowerBound.x, aabb.upperBound.y} + + lines_add(&draw.lines, p1, p2, c) + lines_add(&draw.lines, p2, p3, c) + lines_add(&draw.lines, p3, p4, c) + lines_add(&draw.lines, p4, p1, c) +} + +DrawPolygonFcn :: proc "c" ( + vertices : [^]b2.Vec2, + vertexCount : i32, + color : b2.HexColor, + ctx : rawptr, +) { + + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + + p1 := vertices[vertexCount - 1] + for i in 0 ..< vertexCount { + p2 := vertices[i] + lines_add(&draw.lines, p1, vertices[i], color) + p1 = p2 + } +} + +DrawSolidPolygonFcn :: proc "c" ( + transform : b2.Transform, + vertices : [^]b2.Vec2, + vertexCount : i32, + radius : f32, + color : b2.HexColor, + ctx : rawptr, +) { + + + context = runtime.default_context() + + draw : ^Draw = cast(^Draw)ctx + solid_polygon_add( + &draw.polygons, + transform, + vertices, + vertexCount, + radius, + color, + ) +} + + +DrawCircleFcn :: proc "c" ( + center : b2.Vec2, + radius : f32, + color : b2.HexColor, + ctx : rawptr, +) { + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + circle_add(&draw.circles, center, radius, color) +} + +DrawSolidCircle :: proc( + circle : ^SolidCircle, + transform : b2.Transform, + center : b2.Vec2, + radius : f32, + color : b2.HexColor, +) { + context = runtime.default_context() + + transform := transform + + transform.p = b2.TransformPoint(transform, center) + solid_circle_add(circle, transform, radius, color) +} + +DrawTransform :: proc "c" (lines : ^Lines, transform : b2.Transform) { + context = runtime.default_context() + k_axis_scale : f32 = 0.2 + p1 := transform.p + + p2 := p1 + k_axis_scale * b2.Rot_GetXAxis(transform.q) + lines_add(lines, p1, p2, b2.HexColor.Red) + + p2 = p1 + k_axis_scale * b2.Rot_GetYAxis(transform.q) + lines_add(lines, p1, p2, b2.HexColor.Green) +} + +DrawSolidCircleFcn :: proc "c" ( + transform : b2.Transform, + radius : f32, + color : b2.HexColor, + ctx : rawptr, +) { + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + DrawSolidCircle( + &draw.solid_circles, + transform, + b2.Vec2_zero, + radius, + color, + ) +} + +DrawSolidCapsuleFcn :: proc "c" ( + p1, p2 : b2.Vec2, + radius : f32, + color : b2.HexColor, + ctx : rawptr, +) { + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + solid_capsules_add(&draw.solid_capsules, p1, p2, radius, color) +} + +DrawSegmentFcn :: proc "c" ( + p1, p2 : b2.Vec2, + color : b2.HexColor, + ctx : rawptr, +) { + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + lines_add(&draw.lines, p1, p2, color) +} + +DrawTransformFcn :: proc "c" (transform : b2.Transform, ctx : rawptr) { + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + DrawTransform(&draw.lines, transform) +} + +DrawPointFcn :: proc "c" ( + p : b2.Vec2, + size : f32, + color : b2.HexColor, + ctx : rawptr, +) { + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + points_add(&draw.points, p, size, color) +} + +DrawString :: proc(draw : ^Draw, x, y : int, str: cstring) +{ + draw_list := im.GetForegroundDrawList() + im.DrawList_AddText(draw_list, {f32(x), f32(y)},im.GetColorU32(.Text), str) +} + +DrawStringVec :: proc(draw : ^Draw, p : b2.Vec2, str: cstring) +{ + ps := camera_convert_world_to_screen(&draw.cam, p) + im.DrawList_AddText(im.GetForegroundDrawList(), ps,im.GetColorU32(.Text), str) +} + +DrawStringFcn :: proc "c" ( + p : b2.Vec2, + s : cstring, + color : b2.HexColor, + ctx : rawptr, +) { + context = runtime.default_context() + draw : ^Draw = cast(^Draw)ctx + DrawStringVec(draw, p, s) +} + +draw_flush :: proc(draw : ^Draw) { + + background_draw(&draw.background, &draw.cam) + + solid_circle_flush(&draw.solid_circles, &draw.cam) + solid_polygon_flush(&draw.polygons, &draw.cam) + solid_capsules_flush(&draw.solid_capsules, &draw.cam) + circle_flush(&draw.circles, &draw.cam) + lines_flush(&draw.lines, &draw.cam) + points_flush(&draw.points, &draw.cam) + + + check_opengl() +} + +draw_create :: proc(draw : ^Draw, camera : ^Camera) { + + background_create(&draw.background) + points_create(&draw.points) + solid_capsules_create(&draw.solid_capsules) + lines_create(&draw.lines) + circle_create(&draw.circles) + solid_circle_create(&draw.solid_circles) + solid_polygon_create(&draw.polygons) + + + bounds : b2.AABB = {{-max(f32), -max(f32)}, {max(f32), max(f32)}} + + draw.debug_draw.DrawPolygonFcn = DrawPolygonFcn + draw.debug_draw.DrawSolidPolygonFcn = DrawSolidPolygonFcn + draw.debug_draw.DrawCircleFcn = DrawCircleFcn + draw.debug_draw.DrawSolidCircleFcn = DrawSolidCircleFcn + draw.debug_draw.DrawSolidCapsuleFcn = DrawSolidCapsuleFcn + draw.debug_draw.DrawSegmentFcn = DrawSegmentFcn + draw.debug_draw.DrawTransformFcn = DrawTransformFcn + draw.debug_draw.DrawPointFcn = DrawPointFcn + draw.debug_draw.DrawStringFcn = DrawStringFcn + draw.debug_draw.drawingBounds = bounds + + draw.debug_draw.useDrawingBounds = false + draw.debug_draw.drawShapes = true + draw.debug_draw.drawJoints = true + draw.debug_draw.drawJointExtras = false + draw.debug_draw.drawBounds = false + draw.debug_draw.drawMass = false + draw.debug_draw.drawContacts = false + draw.debug_draw.drawGraphColors = false + draw.debug_draw.drawContactNormals = false + draw.debug_draw.drawContactImpulses = false + draw.debug_draw.drawContactFeatures = false + draw.debug_draw.drawFrictionImpulses = false + draw.debug_draw.drawIslands = false + + draw.drawCounters = true + + draw.debug_draw.userContext = rawptr(draw) + +} + diff --git a/entity.odin b/entity.odin new file mode 100644 index 0000000..d41fbbc --- /dev/null +++ b/entity.odin @@ -0,0 +1,237 @@ +package ion + +import b2 "vendor:box2d" +import array "core:container/small_array" + +static_index :: i32 + +static_index_global :: struct +{ + index : i32, + level : string, + offset : b2.Vec2, +} + +/* + This file contains code to handle box2d stuffs of the game code + Don't put game's logic here +*/ + +revolt_joint_def :: struct +{ + using def : b2.RevoluteJointDef, + + //Everything else can be stored in the def + entity_a, entity_b : static_index, +} + +distance_joint_def :: struct +{ + using def : b2.DistanceJointDef, + + //Everything else can be stored in the def + entity_a, entity_b : static_index, +} + +engine_world :: struct +{ + world_id : b2.WorldId, + + //This in engine code? + static_indexes : map[static_index]int `cbor:"-"`, + relations : map[^static_index][dynamic]static_index_global `cbor:"-"`, + relations_serializeable : map[ static_index][dynamic]static_index_global, + + revolute_joint_defs : [dynamic]revolt_joint_def, + distant_joint_defs : [dynamic]distance_joint_def, + + revolute_joints : [dynamic]b2.JointId, +} + +engine_entity_flags_enum :: enum u64 { + POLYGON_IS_BOX, + MULTI_BODIES, + MULTI_SHAPES, +} + +engine_entity_flags :: bit_set[engine_entity_flags_enum] + +engine_entity_def :: struct { + body_def : b2.BodyDef, + shape_def : b2.ShapeDef, + shape_type : b2.ShapeType, + + + radius, scale : f32, + centers : [2]b2.Vec2, + size : b2.Vec2, + is_loop : bool, + vertices : array.Small_Array(b2.MAX_POLYGON_VERTICES, b2.Vec2), + name_buf : [255]u8 `fmt:"-" cbor:"-"`, + + entity_flags : engine_entity_flags, + + index : static_index, + + body_count : int, +} + +engine_entity :: struct { + body_id : b2.BodyId, + shape_id : b2.ShapeId, + + //This is if the entity has multiple bodies + bodies : [dynamic]b2.BodyId, + shapes : [dynamic]b2.ShapeId, + joints : [dynamic]b2.JointId, + entity_flags : engine_entity_flags, + index : ^static_index, +} + + + +engine_entity_single_body :: proc(def : ^engine_entity_def, world_id: b2.WorldId, index : i32) -> engine_entity +{ + + def := def + + new_entity : engine_entity + + + if def.index != 0 + { + new_entity.index = new(static_index) + new_entity.index^ = def.index + } + + new_entity.body_id = b2.CreateBody(world_id, def.body_def) + + switch def.shape_type{ + + case .circleShape: + { + def.radius *= def.scale + circle := b2.Circle{ radius = def.radius } + + def.scale = 1 + + new_entity.shape_id = b2.CreateCircleShape(new_entity.body_id, def.shape_def, circle) + } + + case .capsuleShape: + { + def.radius *= def.scale + def.centers[0] *= def.scale + def.centers[1] *= def.scale + + def.scale = 1 + + capsule := b2.Capsule{ + center1 = def.centers[0], + center2 = def.centers[1], + radius = def.radius + } + + new_entity.shape_id = b2.CreateCapsuleShape( + new_entity.body_id, + def.shape_def, + capsule + ) + } + case .chainSegmentShape: + { + chain_def := b2.DefaultChainDef() + verts :[dynamic]b2.Vec2 + + for &v in array.slice(&def.vertices){ + v *= def.scale + } + + + for v in array.slice(&def.vertices){ + //If it's not a looped chain then it needs two defination + + if !def.is_loop do append(&verts, v) + + append(&verts, v) + } + + slice := array.slice(&def.vertices) + + chain_def.points = &verts[0] + chain_def.count = i32(len(verts)) + chain_def.isLoop = def.is_loop + + c := b2.CreateChain(new_entity.body_id, chain_def) + + shapes_data :[10]b2.ShapeId + shapes := b2.Body_GetShapes(new_entity.body_id, shapes_data[:]) + + for shape in shapes{ + b2.Shape_SetUserData(shape, rawptr(uintptr(index))) + } + + def.scale = 1 + + + } + case .segmentShape: + { + for &v in array.slice(&def.vertices){ + v *= def.scale + } + segment : b2.Segment = {point1 = array.get(def.vertices, 0), point2 = array.get(def.vertices, 2)} + + new_entity.shape_id = b2.CreateSegmentShape(new_entity.body_id, def.shape_def, segment) + def.scale = 1 + + } + case .polygonShape: + { + poly : b2.Polygon + + if .POLYGON_IS_BOX in def.entity_flags + { + def.size *= def.scale + poly = b2.MakeBox(def.size.x, def.size.y) + def.scale = 1 + }else + { + //def.size *= def.scale + + for &v in array.slice(&def.vertices){ + v *= def.scale + } + + points := make([dynamic]b2.Vec2, 0) + + for p, i in array.slice(&def.vertices){ + if i >= int(def.vertices.len) do break + append_elem(&points, p) + } + sort_points_ccw(points[:]) + + hull := b2.ComputeHull(points[:]) + poly = b2.MakePolygon(hull, 0) + delete(points) + def.scale = 1 + } + new_entity.shape_id = b2.CreatePolygonShape(new_entity.body_id, def.shape_def, poly) + } + + } + + if def.shape_type != .chainSegmentShape{ + b2.Shape_SetUserData(new_entity.shape_id, rawptr(uintptr(index))) + } + + return new_entity +} + + + + + + + + diff --git a/interface.odin b/interface.odin new file mode 100644 index 0000000..8d0d52f --- /dev/null +++ b/interface.odin @@ -0,0 +1,409 @@ +package ion +import "base:runtime" +import "core:slice" +import "core:container/small_array" +import "core:fmt" +import im "shared:odin-imgui" +import "vendor:glfw" +import b2 "vendor:box2d" + +/* +This library will only account for box2d's entities editing + +It only deals with one world_id, which means typically one level +*/ + +EditMode :: enum +{ + ENTITY, + VERTICES, + OVERVIEW, +} + +interface_state :: struct +{ + entity_defs: [dynamic]^engine_entity_def, + entities: [dynamic]^engine_entity, + selected_entity: ^i32, + world: ^engine_world, + state: ^engine_state, + + vertex_index : ^i32, + edit_mode : EditMode, + + curr_revolt_joint : revolt_joint_def, + + curr_joint_joint : distance_joint_def, + + curr_static_index : static_index_global, +} + +interface_body_def_editor :: proc(def: ^engine_entity_def) +{ + if im.BeginCombo("Body Type", fmt.ctprint(def.body_def.type)) + { + for type in b2.BodyType + { + if im.Selectable(fmt.ctprint(type), def.body_def.type == type) do def.body_def.type = type + } + im.EndCombo() + } + + im.SliderFloat2("Position", &def.body_def.position, -50, 50) + + angle := RAD2DEG * b2.Rot_GetAngle(def.body_def.rotation) + + if im.SliderFloat("Rotation", &angle, 0, 359) + { + rad := DEG2RAD * angle + def.body_def.rotation = b2.MakeRot(rad) + } + + im.SliderFloat2("Linear velocity", &def.body_def.linearVelocity, 0, 500) + im.SliderFloat("Angular velocity", &def.body_def.angularVelocity, 0, 500) + im.SliderFloat("Linear Damping", &def.body_def.linearDamping, 0, 500) + im.SliderFloat("Angular Damping", &def.body_def.angularDamping, 0, 500) + im.SliderFloat("Gravity Scale", &def.body_def.gravityScale, 0, 100) + + im.Checkbox("Fixed rotation", &def.body_def.fixedRotation) + + if im.InputText("Body Name", cstring(&def.name_buf[0]), 255) { + def.body_def.name = cstring(&def.name_buf[0]) + } +} + +interface_shape_def_editor :: proc(def: ^engine_entity_def) -> bool +{ + shape_def := &def.shape_def + + + if im.BeginCombo("Shape Type", fmt.ctprint(def.shape_type)) { + + for type in b2.ShapeType + { + if im.Selectable(fmt.ctprint(type), def.shape_type == type) + { + def.shape_type = type + } + } + + im.EndCombo() + } + + switch def.shape_type { + + case .circleShape: + { + im.SliderFloat("radius", &def.radius, 0, 40) + } + + case .polygonShape: + { + im.SliderFloat2("Size", &def.size, -500, 500) + } + + case .capsuleShape: + { + im.SliderFloat2("Center 1", &def.centers[0], -100, 100) + im.SliderFloat2("Center 2", &def.centers[0], -100, 100) + im.SliderFloat("Radius", &def.radius, 0, 40) + } + + case .chainSegmentShape: + { + im.Checkbox("is loop", &def.is_loop) + } + + case .segmentShape: + { + //TODO + } + + } + + im.SliderFloat("Density", &def.shape_def.density, 0, 100) + + if im.Button("Flip horizontally") do flip_points(small_array.slice(&def.vertices), .Horizontal) + if im.Button("Flip Vertically ") do flip_points(small_array.slice(&def.vertices), .Vertical) + + if im.TreeNode("Events and contacts") { + im.Checkbox("Is sensor", &def.shape_def.isSensor) + im.Checkbox("Enable Sensor Events", &def.shape_def.enableSensorEvents) + im.Checkbox("Enable Contact Events", &def.shape_def.enableContactEvents) + im.Checkbox("Enable Hit Events", &def.shape_def.enableHitEvents) + im.Checkbox("Enable Presolve Events", &def.shape_def.enablePreSolveEvents) + im.Checkbox("Invoke contact Creation", &def.shape_def.invokeContactCreation) + im.Checkbox("Update body mass ", &def.shape_def.updateBodyMass) + im.TreePop() + } + + if im.TreeNode("Material") { + im.Separator() + + im.SliderFloat("Friction", &def.shape_def.material.friction, 0, 1) + im.SliderFloat("Restitution", &def.shape_def.material.restitution, 0, 1) + im.SliderFloat("Rolling Resistance", &def.shape_def.material.rollingResistance, 0, 1) + im.SliderFloat("Tangent Speed", &def.shape_def.material.tangentSpeed, 0, 1) + im.InputInt("User material id", &def.shape_def.material.userMaterialId) + + //Colorpicker + + if im.TreeNode("Color") { + color_f32 := u32_to_float4(def.shape_def.material.customColor) + + if im.ColorPicker4("Custom Color", &color_f32, {.Uint8, .InputRGB}) { + def.shape_def.material.customColor = float4_to_u32(color_f32) + } + im.TreePop() + } + + im.Separator() + im.TreePop() + } + + return false +} + +interface_draw_options :: proc(state: ^engine_state) { + if im.BeginTabItem("Controls") { + debug_draw := &state.draw.debug_draw + + im.Checkbox("Shapes", &debug_draw.drawShapes) + im.Checkbox("Joints", &debug_draw.drawJoints) + im.Checkbox("Joint Extras", &debug_draw.drawJointExtras) + im.Checkbox("Bounds", &debug_draw.drawBounds) + im.Checkbox("Contact Points", &debug_draw.drawContacts) + im.Checkbox("Contact Normals", &debug_draw.drawContactNormals) + im.Checkbox("Contact Inpulses", &debug_draw.drawContactImpulses) + im.Checkbox("Contact Features", &debug_draw.drawContactFeatures) + im.Checkbox("Friction Inpulses", &debug_draw.drawFrictionImpulses) + im.Checkbox("Mass ", &debug_draw.drawMass) + im.Checkbox("Body Names", &debug_draw.drawBodyNames) + im.Checkbox("Graph Colors", &debug_draw.drawGraphColors) + im.Checkbox("Islands ", &debug_draw.drawIslands) + + im.SliderFloat("Rotation", &state.draw.cam.rotation, 0, 360) + + im.EndTabItem() + } +} + +interface_edit_static_index :: proc(interface:^interface_state, def: ^engine_entity_def) -> bool +{ + + curr_index := &interface.curr_static_index + entity := interface.entities[interface.selected_entity^] + + level := interface.world + + + if level.relations[entity.index] == nil + { + level.relations[entity.index] = {} + } + + indexes := &level.relations[entity.index] + + if im.InputInt("Index Value", &def.index) do return true + + ret := false + + if def.index != 0 + { + //For now only select from current room + + if im.BeginCombo("Edit Select index", fmt.ctprint(curr_index.index)) + { + for index in level.static_indexes + { + if im.Selectable(fmt.ctprint(index), curr_index.index == index) + { + curr_index.index = index + } + } + im.EndCombo() + } + if curr_index.index != 0 + { + if indexes != nil + { + if im.Button("Add relation") + { + if !slice.contains(indexes[:], interface.curr_static_index) + { + append(indexes, interface.curr_static_index) + } + } + } + } + + if indexes != nil{ + for val, i in indexes + { + im.Text("%d", val.index) + im.SameLine() + if im.Button("Delete") { + ordered_remove(indexes, i) + } + } + } + } + return false +} + +interface_edit_revolute_joint :: proc(interface: ^interface_state) -> bool +{ + + //Select static index and then get bodyId from it + //If chain shapre then allow choosing index + + level := interface.world + + joint_def := &interface.curr_revolt_joint + + if im.BeginCombo("Index A", fmt.ctprint(joint_def.entity_a)) + { + + for i in level.static_indexes + { + if im.Selectable(fmt.ctprint(i), i == joint_def.entity_a) + { + joint_def.entity_a = i + } + } + im.EndCombo() + } + + im.Separator() + + if im.BeginCombo("Index B", fmt.ctprint(joint_def.entity_b)) + { + + for i in level.static_indexes + { + if im.Selectable(fmt.ctprint(i), i == joint_def.entity_b) + { + joint_def.entity_b = i + } + } + im.EndCombo() + } + + //Now box2d + + im.SliderFloat2("localAnchorA", &joint_def.localAnchorA, -5, 5) + im.SliderFloat2("localAnchorB", &joint_def.localAnchorB, -5, 5) + + //Convert to degree to radian + im.SliderFloat("Reference Angle", &joint_def.referenceAngle, 0, 100) + im.SliderFloat("Target Angle", &joint_def.targetAngle, 0, 100) + + im.Checkbox("Enable Spring", &joint_def.enableSpring) + + im.InputFloat("Hertz ", &joint_def.hertz) + + im.InputFloat("Damping Ratio", &joint_def.dampingRatio) + + im.Checkbox("Enable Limit", &joint_def.enableLimit) + + im.InputFloat("Lower Angle", &joint_def.lowerAngle) + im.InputFloat("Upper Angle", &joint_def.upperAngle) + + im.Checkbox("Enable Motor", &joint_def.enableMotor) + im.InputFloat("Moror Torque", &joint_def.maxMotorTorque) + im.InputFloat("Moror Speed", &joint_def.motorSpeed) + im.InputFloat("Draw Size", &joint_def.drawSize) + im.Checkbox("Collide Connected", &joint_def.collideConnected) + + if im.Button("Add joint") + { + append(&level.revolute_joint_defs, interface.curr_revolt_joint) + return true + } + + + return false +} + + +interface_entity :: proc(interface: ^interface_state) -> bool +{ + + entity_selected := interface.selected_entity^ != -1 + + if entity_selected + { + def := interface.entity_defs[interface.selected_entity^] + def_old := def^ + + ret := false + + if im.BeginTabItem("Entity", nil, {.Leading}) + { + + //Flags + for flag in engine_entity_flags_enum + { + contains := flag in def.entity_flags + if im.Checkbox(fmt.ctprint(flag), &contains) + { + def.entity_flags ~= {flag} + } + } + + im.Separator() + + if im.CollapsingHeader("Shape Edit") + { + interface_shape_def_editor(def) + } + + im.Separator() + + if im.CollapsingHeader("Body Edit") + { + interface_body_def_editor(def) + } + + if im.CollapsingHeader("Static Index") + { + ret |= interface_edit_static_index(interface, def) + } + + im.EndTabItem() + } + + if im.BeginTabItem("Joints", nil , {}) + { + + if im.CollapsingHeader("Revolute Joints") + { + ret |= interface_edit_revolute_joint(interface) + } + + im.EndTabItem() + } + + + return def^ != def_old || ret + }else{ + return false + } +} + +interface_all :: proc(interface: ^interface_state) -> bool +{ + ret := false + if im.Begin("Box2d interface") + { + if im.BeginTabBar("Tabs") + { + if interface_entity(interface) do ret = true + + interface_draw_options(interface.state) + im.EndTabBar() + } + } + im.End() + return ret +} diff --git a/ion.odin b/ion.odin new file mode 100644 index 0000000..8765e2c --- /dev/null +++ b/ion.odin @@ -0,0 +1,186 @@ +package ion + + +import "core:encoding/cbor" +import im "shared:odin-imgui" +import "shared:odin-imgui/imgui_impl_glfw" +import "shared:odin-imgui/imgui_impl_opengl3" +import gl "vendor:OpenGL" +import "vendor:glfw" + + + +engine_state :: struct +{ + window : glfw.WindowHandle, + draw : Draw, + restart, pause : bool, + substep_count : u32, + + //Must be set before calling ion_init + width, height : i32, + title : cstring, + time : f32, + tex_line : u32, + drop_callback : glfw.DropProc, + + input : input_state, +} + +MAX_KEYS :: 512 + +input_state :: struct +{ + mouse_wheel : [2]f64, + mouse : [2]f64, + mouse_prev : [2]f64, + + curr, prev : [MAX_KEYS]bool, +} + +/* + This will only be called once to initilize the engine + + initilize graphics library, glfw, callbacks +*/ +engine_init :: proc(state: ^engine_state) +{ + + assert(glfw.Init() == true) + + glfw.WindowHint(glfw.SCALE_TO_MONITOR, 1) + + state.window = glfw.CreateWindow(state.width, state.height, state.title, nil, nil) + + assert(state.window != nil) + + glfw.MakeContextCurrent(state.window) + glfw.SwapInterval(1) + gl.load_up_to(4, 5, glfw.gl_set_proc_address) + + im.CHECKVERSION() + im.CreateContext() + + io := im.GetIO() + + io.ConfigFlags += { + .NavEnableKeyboard, + .NavEnableGamepad, + .DpiEnableScaleFonts, + } + + + im.StyleColorsClassic() + + style := im.GetStyle() + style.ChildBorderSize = 0. + style.ChildRounding = 6 + style.TabRounding = 6 + style.FrameRounding = 6 + style.GrabRounding = 6 + style.WindowRounding = 6 + style.PopupRounding = 6 + + imgui_impl_glfw.InitForOpenGL(state.window, true) + imgui_impl_opengl3.Init("#version 150") + + state.draw.cam = camera_init() + + display_w, display_h := glfw.GetFramebufferSize(state.window) + state.draw.cam.width = display_w + state.draw.cam.height = display_h + state.draw.cam.zoom = 15 + state.draw.show_ui = true + + draw_create(&state.draw, &state.draw.cam) + + cbor.tag_register_type({ + marshal = proc(_: ^cbor.Tag_Implementation, e: cbor.Encoder, v: any) -> cbor.Marshal_Error { + cbor._encode_u8(e.writer, 201, .Tag) or_return + return nil; + }, + unmarshal = proc(_: ^cbor.Tag_Implementation, d: cbor.Decoder, _: cbor.Tag_Number, v: any) -> (cbor.Unmarshal_Error) { + return nil + }, + }, 201, rawptr) +} + +update_frame :: proc(state: ^engine_state) +{ + state.input.mouse_wheel = {} + glfw.PollEvents() + + keyboard_update(state) + + gl.ClearColor(0.4, 0.5, 0.6, 1.0) + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + + cam := &state.draw.cam + cam.width, cam.height = glfw.GetWindowSize(state.window) + + state.width , state.height = glfw.GetFramebufferSize(state.window) + gl.Viewport(0, 0, state.width, state.height) + + imgui_impl_opengl3.NewFrame() + imgui_impl_glfw.NewFrame() + im.NewFrame() +} + +end_frame :: proc(state: ^engine_state) +{ + im.Render() + imgui_impl_opengl3.RenderDrawData(im.GetDrawData()) + glfw.SwapBuffers(state.window) +} + +cleanup :: proc(state: ^engine_state) +{ + imgui_impl_opengl3.Shutdown() + imgui_impl_glfw.Shutdown() +} + +engine_should_close :: proc(state : ^engine_state) -> b32 +{ + return glfw.WindowShouldClose(state.window) +} + + +keyboard_update :: proc(state: ^engine_state) +{ + state.input.mouse_prev = state.input.mouse + + state.input.mouse.x, state.input.mouse.y = glfw.GetCursorPos(state.window) + + state.input.prev = state.input.curr + + //Update current states + + for key in glfw.KEY_SPACE ..< MAX_KEYS + { + state.input.curr[key] = glfw.GetKey(state.window, i32(key)) == glfw.PRESS + } + + for key in 0.. bool{ + return state.input.curr[key] +} + +is_key_pressed :: #force_inline proc(state: ^engine_state, key : i32) -> bool{ + return state.input.curr[key] && !state.input.prev[key] +} + +is_key_released :: #force_inline proc(state: ^engine_state, key : i32) -> bool{ + return !state.input.curr[key] && state.input.prev[key] +} + + + + + + + diff --git a/misc.odin b/misc.odin new file mode 100644 index 0000000..d4833c3 --- /dev/null +++ b/misc.odin @@ -0,0 +1,90 @@ +package ion + +import "core:slice" +import b2 "vendor:box2d" + + +saturate :: proc(f : f32) -> f32 { + return (f < 0.0) ? 0.0 : (f > 1.0) ? 1.0 : f +} + +f32_to_u8_sat :: proc(val : f32) -> u8 { + + sat := saturate(val) + sat *= 255 + sat += 0.5 + + ret := cast(u8)sat + return ret +} + + +float4_to_u32 :: proc(color : [4]f32) -> u32 { + out : u32 + out = u32(f32_to_u8_sat(color.a)) << 24 + out |= u32(f32_to_u8_sat(color.r)) << 16 + out |= u32(f32_to_u8_sat(color.g)) << 8 + out |= u32(f32_to_u8_sat(color.b)) + return out +} + + +u32_to_float4 :: proc(color : u32) -> [4]f32 { + ret : [4]f32 + ret.a = f32((color >> 24) & 0xFF) / 255.0 + ret.r = f32((color >> 16) & 0xFF) / 255.0 + ret.g = f32((color >> 8) & 0xFF) / 255.0 + ret.b = f32((color) & 0xFF) / 255.0 + return ret +} + + + +centroid :: proc(points: []b2.Vec2) -> b2.Vec2{ + center := b2.Vec2{0,0} + for p in points do center += p + center /= f32(len(points)) + return center +} + + +cross :: proc(o, a, b : b2.Vec2) -> f32{ + return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) +} + +//For sorting +curr_center : b2.Vec2 + +sort_points_ccw :: proc(points : []b2.Vec2){ + if len(points) == 0 do return + + curr_center = centroid(points) + slice.sort_by(points , proc(a, b: b2.Vec2) -> bool{ + c := cross(curr_center, a, b) + + if abs(c) < 1e-7{ + return b2.Distance(curr_center, a) < b2.Distance(curr_center, b) + } + return c > 0 + }) +} + +FlipDirection :: enum { + Horizontal, + Vertical, + Both, // Flip both horizontally and vertically +} + + +flip_points :: proc(points: []b2.Vec2, direction : FlipDirection){ + for &vertex, i in points{ + switch direction { + case .Horizontal: + points[i] = b2.Vec2{-vertex.x, vertex.y} + case .Vertical: + points[i] = b2.Vec2{vertex.x, -vertex.y} + case .Both: + points[i] = b2.Vec2{-vertex.x, -vertex.y} + } + } +} diff --git a/shaders/background.fs b/shaders/background.fs new file mode 100644 index 0000000..b8a156e --- /dev/null +++ b/shaders/background.fs @@ -0,0 +1,29 @@ +#version 330 + +out vec4 FragColor; + +uniform float time; +uniform vec2 resolution; +uniform vec3 baseColor; + + + +//A simple pseudo-random function +float random(vec2 st){ + return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5853123); +} + +void main(){ + vec2 uv = gl_FragCoord.xy / resolution.xy; + + //Create some noise + float noise = random(uv + time * 0.5); + + //Adjust these values to control the intensity and color of the grain + float grainIntensity = 0.01; + + //Mix the base color with the noise + vec3 color = baseColor + vec3(noise * grainIntensity); + + FragColor = vec4(color, 1.0); +} \ No newline at end of file diff --git a/shaders/background.vs b/shaders/background.vs new file mode 100644 index 0000000..8574b21 --- /dev/null +++ b/shaders/background.vs @@ -0,0 +1,7 @@ +#version 330 + +layout(location = 0) in vec2 v_position; + +void main(void){ + gl_Position = vec4(v_position, 0.0f, 1.0f); +} \ No newline at end of file diff --git a/shaders/circle.fs b/shaders/circle.fs new file mode 100644 index 0000000..66491a5 --- /dev/null +++ b/shaders/circle.fs @@ -0,0 +1,19 @@ +#version 330 + +in vec2 f_position; +in vec4 f_color; +in float f_thickness; + + +out vec4 fragColor; + +void main(){ + float radius = 1.0; + + //distance to circle + vec2 w = f_position; + float dw = length(w); + float d = abs(dw - radius); + + fragColor = vec4(f_color.rgb, smoothstep(f_thickness, 0.0, d)); +} \ No newline at end of file diff --git a/shaders/circle.vs b/shaders/circle.vs new file mode 100644 index 0000000..eb77635 --- /dev/null +++ b/shaders/circle.vs @@ -0,0 +1,29 @@ +#version 330 + +uniform mat4 projectionMatrix; +uniform float pixelScale; + +layout(location = 0) in vec2 v_localPosition; +layout(location = 1) in vec2 v_instancePosition; +layout(location = 2) in float v_instanceRadius; +layout(location = 3) in vec4 v_instanceColor; + + +out vec2 f_position; +out vec4 f_color; +out float f_thickness; + + +void main(){ + f_position = v_localPosition; + f_color = v_instanceColor; + float radius = v_instanceRadius; + + //resolution.y = pixelScale * radius + + f_thickness = 3.0f / (pixelScale * radius); + + vec2 p = vec2(radius * v_localPosition.x, radius * v_localPosition.y) + v_instancePosition; + + gl_Position = projectionMatrix * vec4(p, 0.0f, 1.0f); +} \ No newline at end of file diff --git a/shaders/solid_capsule.fs b/shaders/solid_capsule.fs new file mode 100644 index 0000000..35f44c7 --- /dev/null +++ b/shaders/solid_capsule.fs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2024 Erin Catto +// SPDX-License-Identifier: MIT + +#version 330 + +in vec2 f_position; +in vec4 f_color; +in float f_length; +in float f_thickness; + +out vec4 color; + +// Thanks to baz and kolyan3040 for help on this shader +// todo this can be optimized a bit, keeping some terms for clarity + +// https://en.wikipedia.org/wiki/Alpha_compositing +vec4 blend_colors(vec4 front,vec4 back) +{ + vec3 cSrc = front.rgb; + float alphaSrc = front.a; + vec3 cDst = back.rgb; + float alphaDst = back.a; + + vec3 cOut = cSrc * alphaSrc + cDst * alphaDst * (1.0 - alphaSrc); + float alphaOut = alphaSrc + alphaDst * (1.0 - alphaSrc); + + // remove alpha from rgb + cOut = cOut / alphaOut; + + return vec4(cOut, alphaOut); +} + +void main() +{ + // radius in unit quad + float radius = 0.5 * (2.0 - f_length); + + vec4 borderColor = f_color; + vec4 fillColor = 0.6f * borderColor; + + vec2 v1 = vec2(-0.5 * f_length, 0); + vec2 v2 = vec2(0.5 * f_length, 0); + + // distance to line segment + vec2 e = v2 - v1; + vec2 w = f_position - v1; + float we = dot(w, e); + vec2 b = w - e * clamp(we / dot(e, e), 0.0, 1.0); + float dw = length(b); + + // SDF union of capsule and line segment + float d = min(dw, abs(dw - radius)); + + // roll the fill alpha down at the border + vec4 back = vec4(fillColor.rgb, fillColor.a * smoothstep(radius + f_thickness, radius, dw)); + + // roll the border alpha down from 1 to 0 across the border thickness + vec4 front = vec4(borderColor.rgb, smoothstep(f_thickness, 0.0f, d)); + + color = blend_colors(front, back); +} \ No newline at end of file diff --git a/shaders/solid_capsule.vs b/shaders/solid_capsule.vs new file mode 100644 index 0000000..c592fcc --- /dev/null +++ b/shaders/solid_capsule.vs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Erin Catto +// SPDX-License-Identifier: MIT + +#version 330 + +uniform mat4 projectionMatrix; +uniform float pixelScale; + +layout(location=0) in vec2 v_localPosition; +layout(location=1) in vec4 v_instanceTransform; +layout(location=2) in float v_instanceRadius; +layout(location=3) in float v_instanceLength; +layout(location=4) in vec4 v_instanceColor; + +out vec2 f_position; +out vec4 f_color; +out float f_length; +out float f_thickness; + +void main() +{ + f_position = v_localPosition; + f_color = v_instanceColor; + + float radius = v_instanceRadius; + float length = v_instanceLength; + + // scale quad large enough to hold capsule + float scale = radius + 0.5 * length; + + // quad range of [-1, 1] implies normalize radius and length + f_length = length / scale; + + // resolution.y = pixelScale * scale + f_thickness = 3.0f / (pixelScale * scale); + + float x = v_instanceTransform.x; + float y = v_instanceTransform.y; + float c = v_instanceTransform.z; + float s = v_instanceTransform.w; + vec2 p = vec2(scale * v_localPosition.x, scale * v_localPosition.y); + p = vec2((c * p.x - s * p.y) + x, (s * p.x + c * p.y) + y); + gl_Position = projectionMatrix * vec4(p, 0.0, 1.0); +} diff --git a/shaders/solid_circle.fs b/shaders/solid_circle.fs new file mode 100644 index 0000000..695c884 --- /dev/null +++ b/shaders/solid_circle.fs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Erin Catto +// SPDX-License-Identifier: MIT + +#version 330 + +in vec2 f_position; +in vec4 f_color; +in float f_thickness; + +out vec4 fragColor; + +// https://en.wikipedia.org/wiki/Alpha_compositing +vec4 blend_colors(vec4 front, vec4 back) +{ + vec3 cSrc = front.rgb; + float alphaSrc = front.a; + vec3 cDst = back.rgb; + float alphaDst = back.a; + + vec3 cOut = cSrc * alphaSrc + cDst * alphaDst * (1.0 - alphaSrc); + float alphaOut = alphaSrc + alphaDst * (1.0 - alphaSrc); + cOut = cOut / alphaOut; + + return vec4(cOut, alphaOut); +} + +void main() +{ + // radius in unit quad + float radius = 1.0; + + // distance to axis line segment + vec2 e = vec2(radius, 0); + vec2 w = f_position; + float we = dot(w, e); + vec2 b = w - e * clamp(we / dot(e, e), 0.0, 1.0); + float da = length(b); + + // distance to circle + float dw = length(w); + float dc = abs(dw - radius); + + // union of circle and axis + float d = min(da, dc); + + vec4 borderColor = f_color; + vec4 fillColor = 0.6f * borderColor; + + // roll the fill alpha down at the border + vec4 back = vec4(fillColor.rgb, fillColor.a * smoothstep(radius + f_thickness, radius, dw)); + + // roll the border alpha down from 1 to 0 across the border thickness + vec4 front = vec4(borderColor.rgb, smoothstep(f_thickness, 0.0f, d)); + + fragColor = blend_colors(front, back); +} diff --git a/shaders/solid_circle.vs b/shaders/solid_circle.vs new file mode 100644 index 0000000..9258e08 --- /dev/null +++ b/shaders/solid_circle.vs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2024 Erin Catto +// SPDX-License-Identifier: MIT + +#version 330 + +uniform mat4 projectionMatrix; +uniform float pixelScale; + +layout(location = 0) in vec2 v_localPosition; +layout(location = 1) in vec4 v_instanceTransform; +layout(location = 2) in float v_instanceRadius; +layout(location = 3) in vec4 v_instanceColor; + +out vec2 f_position; +out vec4 f_color; +out float f_thickness; + +void main() +{ + f_position = v_localPosition; + f_color = v_instanceColor; + float radius = v_instanceRadius; + + // resolution.y = pixelScale * radius + f_thickness = 3.0f / (pixelScale * radius); + + float x = v_instanceTransform.x; + float y = v_instanceTransform.y; + float c = v_instanceTransform.z; + float s = v_instanceTransform.w; + vec2 p = vec2(radius * v_localPosition.x, radius * v_localPosition.y); + p = vec2((c * p.x - s * p.y) + x, (s * p.x + c * p.y) + y); + gl_Position = projectionMatrix * vec4(p, 0.0f, 1.0f); +} + diff --git a/shaders/solid_polygons.fs b/shaders/solid_polygons.fs new file mode 100644 index 0000000..021c9f3 --- /dev/null +++ b/shaders/solid_polygons.fs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2024 Erin Catto +// SPDX-License-Identifier: MIT + +#version 330 + +in vec2 f_position; +in vec2 f_points[8]; +flat in int f_count; +in float f_radius; +in vec4 f_color; +in float f_thickness; + +out vec4 fragColor; + +// https://en.wikipedia.org/wiki/Alpha_compositing +vec4 blend_colors(vec4 front, vec4 back) +{ + vec3 cSrc = front.rgb; + float alphaSrc = front.a; + vec3 cDst = back.rgb; + float alphaDst = back.a; + + vec3 cOut = cSrc * alphaSrc + cDst * alphaDst * (1.0 - alphaSrc); + float alphaOut = alphaSrc + alphaDst * (1.0 - alphaSrc); + + // remove alpha from rgb + cOut = cOut / alphaOut; + + return vec4(cOut, alphaOut); +} + +float cross2d(in vec2 v1, in vec2 v2) +{ + return v1.x * v2.y - v1.y * v2.x; +} + +// Signed distance function for convex polygon +float sdConvexPolygon(in vec2 p, in vec2[8] v, in int count) +{ + // Initial squared distance + float d = dot(p - v[0], p - v[0]); + + // Consider query point inside to start + float side = -1.0; + int j = count - 1; + for (int i = 0; i < count; ++i) + { + // Distance to a polygon edge + vec2 e = v[i] - v[j]; + vec2 w = p - v[j]; + float we = dot(w, e); + vec2 b = w - e * clamp(we / dot(e, e), 0.0, 1.0); + float bb = dot(b, b); + + // Track smallest distance + if (bb < d) + { + d = bb; + } + + // If the query point is outside any edge then it is outside the entire polygon. + // This depends on the CCW winding order of points. + float s = cross2d(w, e); + if (s >= 0.0) + { + side = 1.0; + } + + j = i; + } + + return side * sqrt(d); +} + +void main() +{ + vec4 borderColor = f_color; + vec4 fillColor = 0.6f * borderColor; + + float dw = sdConvexPolygon(f_position, f_points, f_count); + float d = abs(dw - f_radius); + + // roll the fill alpha down at the border + vec4 back = vec4(fillColor.rgb, fillColor.a * smoothstep(f_radius + f_thickness, f_radius, dw)); + + // roll the border alpha down from 1 to 0 across the border thickness + vec4 front = vec4(borderColor.rgb, smoothstep(f_thickness, 0.0f, d)); + + fragColor = blend_colors(front, back); + + // todo debugging + // float resy = 3.0f / f_thickness; + + // if (resy < 539.9f) + // { + // fragColor = vec4(1, 0, 0, 1); + // } + // else if (resy > 540.1f) + // { + // fragColor = vec4(0, 1, 0, 1); + // } + // else + // { + // fragColor = vec4(0, 0, 1, 1); + // } +} diff --git a/shaders/solid_polygons.vs b/shaders/solid_polygons.vs new file mode 100644 index 0000000..62dce44 --- /dev/null +++ b/shaders/solid_polygons.vs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2024 Erin Catto +// SPDX-License-Identifier: MIT + +#version 330 + +uniform mat4 projectionMatrix; +uniform float pixelScale; + +layout(location = 0) in vec2 v_localPosition; +layout(location = 1) in vec4 v_instanceTransform; +layout(location = 2) in vec4 v_instancePoints12; +layout(location = 3) in vec4 v_instancePoints34; +layout(location = 4) in vec4 v_instancePoints56; +layout(location = 5) in vec4 v_instancePoints78; +layout(location = 6) in int v_instanceCount; +layout(location = 7) in float v_instanceRadius; +layout(location = 8) in vec4 v_instanceColor; + +out vec2 f_position; +out vec4 f_color; +out vec2 f_points[8]; +flat out int f_count; +out float f_radius; +out float f_thickness; + +void main() +{ + f_position = v_localPosition; + f_color = v_instanceColor; + + f_radius = v_instanceRadius; + f_count = v_instanceCount; + + f_points[0] = v_instancePoints12.xy; + f_points[1] = v_instancePoints12.zw; + f_points[2] = v_instancePoints34.xy; + f_points[3] = v_instancePoints34.zw; + f_points[4] = v_instancePoints56.xy; + f_points[5] = v_instancePoints56.zw; + f_points[6] = v_instancePoints78.xy; + f_points[7] = v_instancePoints78.zw; + + // Compute polygon AABB + vec2 lower = f_points[0]; + vec2 upper = f_points[0]; + for (int i = 1; i < v_instanceCount; ++i) + { + lower = min(lower, f_points[i]); + upper = max(upper, f_points[i]); + } + + vec2 center = 0.5 * (lower + upper); + vec2 width = upper - lower; + float maxWidth = max(width.x, width.y); + + float scale = f_radius + 0.5 * maxWidth; + float invScale = 1.0 / scale; + + // Shift and scale polygon points so they fit in 2x2 quad + for (int i = 0; i < f_count; ++i) + { + f_points[i] = invScale * (f_points[i] - center); + } + + // Scale radius as well + f_radius = invScale * f_radius; + + // resolution.y = pixelScale * scale + f_thickness = 3.0f / (pixelScale * scale); + + // scale up and transform quad to fit polygon + float x = v_instanceTransform.x; + float y = v_instanceTransform.y; + float c = v_instanceTransform.z; + float s = v_instanceTransform.w; + vec2 p = vec2(scale * v_localPosition.x, scale * v_localPosition.y) + center; + p = vec2((c * p.x - s * p.y) + x, (s * p.x + c * p.y) + y); + gl_Position = projectionMatrix * vec4(p, 0.0f, 1.0f); +}