This commit is contained in:
SamratGhale
2026-03-10 22:11:28 +05:45
commit d23c934662
16 changed files with 3051 additions and 0 deletions
+19
View File
@@ -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
+1645
View File
File diff suppressed because it is too large Load Diff
+237
View File
@@ -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
}
+409
View File
@@ -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
}
+186
View File
@@ -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..<glfw.KEY_SPACE
{
state.input.curr[key] = glfw.GetMouseButton(state.window, i32(key)) == glfw.PRESS
}
}
is_key_down :: #force_inline proc(state: ^engine_state, key : i32) -> 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]
}
+90
View File
@@ -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}
}
}
}
+29
View File
@@ -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);
}
+7
View File
@@ -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);
}
+19
View File
@@ -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));
}
+29
View File
@@ -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);
}
+61
View File
@@ -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);
}
+44
View File
@@ -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);
}
+56
View File
@@ -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);
}
+35
View File
@@ -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);
}
+106
View File
@@ -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);
// }
}
+79
View File
@@ -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);
}