Transform shenanigans

main
Cameron Murphy Reikes 2 years ago
parent 1b3e388c1d
commit f2cb1fdd31

@ -1,6 +0,0 @@
#import "Basic";
#import "Math";
#import,file "testkit.jai";
#import,file "physics.jai";
main :: ()

@ -1,6 +1,9 @@
#import "Basic"; #import "Window_Creation";
Debug :: #import "Debug";
Simp :: #import "Simp";
Input :: #import "Input";
#import "Math"; #import "Math";
#import,file "testkit.jai"; #import "Basic";
TIMESTEP: float = 1.0 / 60.0; TIMESTEP: float = 1.0 / 60.0;
DEBUGGING :: true; DEBUGGING :: true;
@ -50,11 +53,134 @@ hit_impulse :: (body_pos: Vector2, mouse_pos: Vector2) -> Vector2 {
return .{0, -15.0}; return .{0, -15.0};
//return normalize(body_pos - mouse_pos) * 15.0; //return normalize(body_pos - mouse_pos) * 15.0;
} }
ShapeType :: enum { ShapeType :: enum {
Circle; Circle;
Rectangle; Rectangle;
}; };
degrees :: (d: float) -> float {
return d * PI/180.0;
}
/*
A transform is an offset, a rotation, and a scaling
transforms define new basis vectors which in turn define a new space, the space of that transform
when a transform transforms a point in one space, it moves it into its space. It takes whatever point you give it, and acts as if its offset was from the transform's point of view, not where the point came from.
An entity's transform, when applied to a point, converts from local to world space. The input is local, the output is world
The inverse does the opposite, world to local space.
*/
Transform2D :: struct {
pos: Vector2;
angle: float;
scale: Vector2 = .{1.0, 1.0};
};
approx_eq :: (v1: Vector2, v2: Vector2) -> bool {
return abs(v1.x - v2.x) < 0.01 && abs(v1.y - v2.y) < 0.01;
}
#run {
point := xy(5, 6);
t := Transform2D.{pos = .{1, 2}, angle = 0.0, scale = .{2.0, 1.0}};
print("% | % | %\n", inv(t), xform(inv(t), xform(t, point)), point);
assert(approx_eq(point, xform(inv(t), xform(t, point))));
}
inv :: (t: Transform2D) -> Transform2D {
// v is a vector, v || s means xy(v.x*s.x, v.y*s.y), elementwise product. For
// scaling vector
// inv(t) * t * pos = pos
// inv(t) * ( rotate(xy(pos.x*t.scale.x, pos.y*t.scale.y), t.angle) + t.pos ) = pos
// p' = rotate(xy(pos.x*t.scale.x, pos.y*t.scale.y), t.angle) + t.pos
// inv(t) * p' = pos
// in = inv(t)
// rotate(xy(p'.x*in.scale.x, p'.y*in.scale.y), in.angle) + in.pos = pos
// rotate(xy(p'.x*in.scale.x, p'.y*in.scale.y), in.angle) = pos - in.pos
// p' || in.scale = rotate(pos - in.pos, -in.angle)
// p' = rotate(pos - in.pos, -in.angle) || 1.0/in.scale
// rotate(xy(pos.x*t.scale.x, pos.y*t.scale.y), t.angle) + t.pos = rotate(pos - in.pos, -in.angle) || 1.0/in.scale
// rotate(pos || t.scale, t.angle) + t.pos = rotate(pos - in.pos, -in.angle) || 1.0/in.scale
// rotate(pos - in.pos, -in.angle) || 1.0/in.scale = rotate(pos || t.scale, t.angle) + t.pos
// ^^ trying to find all the in., so want to make the left side look like the right side
return .{
pos = -t.pos,
scale = .{1.0/t.scale.x, 1.0/t.scale.y},
angle = -t.angle,
};
// THIS IS WRONG: I think that: rotate(v, theta) || scale == rotate(v || scale, theta)
V :: Vector2.{0.5, 0.6};
THETA :: #run degrees(39.0);
SCALE :: Vector2.{2.0, 3.0};
apply_scale :: (to: Vector2, scale: Vector2) -> Vector2 {
return .{to.x*scale.x, to.y*scale.y};
}
#run {
/*
rotation:
x' = x * cos(angle) + y * -sin(angle)
y' = x * sin(angle) + y * cos(angle)
want to solve for inner
rotate(v, theta) || scale_in = rotate(v || inner, theta)
where x = v.x, y = v.y
(x * cos(angle) + y * -sin(angle))*scale_in.x = x * inner.x * cos(angle) + y * inner.y -sin(angle)
(x * sin(angle) + y * cos(angle))*scale_in.y = x * inner.x * sin(angle) + y * inner.y * cos(angle)
system of equations, all variables known except for inner. Solving for inner.x:
((x * cos(angle) + y * -sin(angle))*scale_in.x - y * inner.y -sin(angle)) / (x * cos(angle)) = inner.x
now reinserting into the other equation...
(x * sin(angle) + y * cos(angle))*scale_in.y = x * ((x * cos(angle) + y * -sin(angle))*scale_in.x - y * inner.y -sin(angle)) / (x * cos(angle)) * sin(angle) + y * inner.y * cos(angle)
(x * sin(angle) + y * cos(angle))*scale_in.y = (x * (x * cos(angle) + y * -sin(angle))*scale_in.x - x * y * inner.y -sin(angle)) / (x * cos(angle)) * sin(angle) + y * inner.y * cos(angle)
*/
/*
left := apply_scale(rotate(V, THETA), SCALE);
right := rotate(apply_scale(V, SCALE), THETA);
print("% %\n", left, right);
assert(approx_eq(left, right));*/
}
//#assert approx_eq(apply_scale(rotate(V, THETA), SCALE), rotate(apply_scale(V, SCALE), THETA));
return .{};
/*return .{
pos = -
}*/
}
xform :: (using t: Transform2D, p: Vector2) -> Vector2 {
result: Vector2 = p;
result.x *= scale.x;
result.y *= scale.y;
result = rotate(result, angle);
result += pos;
return result;
}
xform :: (from: Transform2D, other: Transform2D) -> Transform2D {
result: Transform2D;
result.pos = xform(from, other.pos);
result.angle = from.angle + other.angle;
result.scale.x = from.scale.x * other.scale.x;
result.scale.y = from.scale.y * other.scale.y;
return result;
}
xform_inv :: (using t: Transform2D, p: Vector2) -> Vector2 {
result: Vector2 = p;
result -= pos;
result = rotate(result, -angle);
result.x /= scale.x;
result.y /= scale.y;
return result;
}
Shape :: struct { Shape :: struct {
type: ShapeType; type: ShapeType;
offset: Vector2; // in local space of body offset: Vector2; // in local space of body
@ -156,7 +282,243 @@ resolve :: (using c: Contact) {
body_b.pos -= ds * tb; body_b.pos -= ds * tb;
} }
draw_body :: (d: DrawingSettings, using b: Body) { #scope_file;
// ------------------------------------------------------------------------
// demo stuff visualization/debugging stuff
window: Window_Type;
quit: bool;
dt: float;
panning: bool = false;
window_width: s32 = 1280;
window_height: s32 = 720;
// the camera is a transform which goes from its parent space (screen) to its space (world space)
camera: Transform2D = .{scale = .{0.01, 0.01}, pos = #run -xy(window_width, window_height)/2.0 * 0.01};
mouse_delta_screen: Vector2;
mouse_frozen: bool;
mouse_frozen_at: Vector2;
last_time: float64;
last_mouse_pos: Vector2;
dbgprint :: print; // so can be detected and removed
#scope_file;
normalize :: (using v: *Vector2) -> float {
sq := sqrt(x*x + y*y);
factor := 1.0 / sq;
x *= factor;
y *= factor;
return sq;
}
normalize :: (v: Vector2) -> Vector2 #must {
normalize(*v);
return v;
}
xy :: (x: int, y: int) -> Vector2 {
return xy(cast(float)x, cast(float)y);
}
#scope_export;
WHITE :: Vector4.{1.0, 1.0, 1.0, 1.0};
RED :: Vector4.{1.0, 0.0, 0.0, 1.0};
GREEN :: Vector4.{0.0, 1.0, 0.0, 1.0};
BLUE :: Vector4.{0.0, 0.0, 1.0, 1.0};
ORANGE :: Vector4.{1.0, cast(float)0xA5/ cast(float)255, 0.0, 1.0};
DrawingSettings :: struct {
world_space: bool = true;
color: Vector4 = WHITE;
};
line :: (using d: DrawingSettings, _from: Vector2, _to: Vector2, thickness: float = 1.0) {
width := thickness;
from := _from;
to := _to;
if world_space {
from = world_to_screen(from);
to = world_to_screen(to);
}
Simp.set_shader_for_color(true);
normal := rotate(unit_vector(to - from), PI/2.0);
Simp.immediate_quad(from + normal*width, from - normal*width, to + normal*width, to - normal*width, color = color);
}
vector :: (d: DrawingSettings, from: Vector2, vector: Vector2) {
line(d, from, from+vector);
arrow_head_length := length(vector) * 0.5;
// arrow head
line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, PI/2.0 + PI/4.0));
line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, -PI/2.0 + -PI/4.0));
}
LastingPip :: struct {
alive_for: float;
pos: Vector2;
d: DrawingSettings;
};
pips : [100] LastingPip;
push_pip :: (d: DrawingSettings, at: Vector2) {
for * pips
{
if it.alive_for <= 0.0
{
it.alive_for = 1.0;
it.pos = at;
it.d = d;
break;
}
}
}
draw_pips :: (dt: float) {
for * pips if it.alive_for > 0.0
{
d := it.d;
d.color.w *= it.alive_for;
pip(d, it.pos);
it.alive_for -= dt;
}
}
pip :: (using d: DrawingSettings, _at: Vector2, size: float = 0.05) {
at := _at;
Simp.set_shader_for_color(true);
if world_space {
at = world_to_screen(at);
size /= camera.scale.x;
}
Simp.immediate_quad(at + xy(-size, size), at + xy(size, size), at + xy(size, -size), at + xy(-size, -size), color = color);
}
font: *Simp.Dynamic_Font;
text :: (using d: DrawingSettings, t: string, p: Vector2) {
drawing_at := p;
if world_space {
drawing_at = world_to_screen(drawing_at);
}
Simp.draw_text(font, xx drawing_at.x, xx drawing_at.y, t, color = color);
}
init :: () {
window = create_window(window_width, window_height, "Test Kit");
// Actual render size in pixels can be different from the window dimensions we specified above (for example on high-resolution displays on macOS/iOS).
window_width, window_height = Simp.get_render_dimensions(window);
Simp.set_render_target(window);
font = Simp.get_font_at_size(".", "Roboto-Regular.ttf", 18);
assert(font != null);
}
// screen coordinates
mouse_screen :: () -> Vector2 {
if mouse_frozen return mouse_frozen_at;
x, y := get_mouse_pointer_position();
pos: Vector2 = xy(cast(float)x, cast(float)y);
// simp is lower left is (0, 0) and y+ is up
pos.y = window_height - pos.y;
return pos;
}
mouse_world :: () -> Vector2 {
return screen_to_world(mouse_screen());
}
// query quit to see if should quit. Call this in a loop
frame_start :: () {
dt = cast(float)(get_time() - last_time);
last_time = get_time();
Input.update_window_events();
mouse_delta_screen = mouse_screen() - last_mouse_pos; // using Input.mouse_delta_x seems to incorrectly accumulate
last_mouse_pos = mouse_screen();
for Input.get_window_resizes() {
Simp.update_window(it.window); // Simp will do nothing if it doesn't care about this window.
if it.window == window {
should_reinit := (it.width != window_width) || (it.height != window_height);
window_width = it.width;
window_height = it.height;
//if should_reinit my_init_fonts(); // Resize the font for the new window size.
}
}
camera.scale.x *= 1.0 - 0.1*Input.mouse_delta_z/120.0;
camera.scale.y = camera.scale.x;
if panning {
camera.pos -= mouse_delta_screen*camera.scale.x;
}
Simp.immediate_begin();
Simp.clear_render_target(0.0, 0.0, 0.0, 1.0);
// draw grid
grid_d: DrawingSettings = .{true,.{0.2, 0.2, 0.2, 0.5}};
text_d := grid_d;
text_d.color = .{0.6, 0.6, 0.6, 1.0};
help_text_max :: 8;
help_zoom_max :: 0.04;
for x: -30..30 {
line(grid_d, xy(x, 30), xy(x, -30));
if camera.scale.x <= help_zoom_max && abs(x) < help_text_max then text(text_d, tprint("%m", x), .{xx x, 0.0});
}
for y: -30..30 {
line(grid_d, xy(30, y), xy(-30, y));
if camera.scale.x <= help_zoom_max && abs(y) < help_text_max then text(text_d, tprint("%m", y), .{0.0, xx y});
}
draw_pips(dt);
if mouse_frozen pip(.{false, ORANGE}, mouse_frozen_at, size = 20.0);
// handle inputs
for Input.events_this_frame {
if it.type == .QUIT then quit = true;
if it.type == {
case .KEYBOARD;
if it.key_pressed {
if it.key_code == {
case .ESCAPE;
quit = true;
case #char "T";
if !mouse_frozen mouse_frozen_at = mouse_screen();
mouse_frozen = !mouse_frozen;
}
}
if it.key_code == .MOUSE_BUTTON_LEFT {
panning = cast(bool)it.key_pressed;
}
}
}
}
frame_end :: () {
Simp.immediate_flush();
Simp.swap_buffers(window);
}
screen_to_world :: (screen: Vector2) -> Vector2 {
return xform(camera, screen);
}
world_to_screen :: (world: Vector2) -> Vector2 {
return xform_inv(camera, world);
}
draw_body :: (d: DrawingSettings, b: Body) {
draw_shape(d, b.shape);
}
draw_shape :: (d: DrawingSettings, shape: Shape) {
#if false {
if shape.type == { if shape.type == {
case .Circle; case .Circle;
points: int = 32; points: int = 32;
@ -187,9 +549,10 @@ draw_body :: (d: DrawingSettings, using b: Body) {
line(d, lower_left, upper_left); line(d, lower_left, upper_left);
} }
} }
}
circledemo :: () { demo_circle :: () {
init(); init();
bodies: [..]Body; bodies: [..]Body;
@ -278,4 +641,124 @@ circledemo :: () {
} }
} }
main :: () { circledemo(); } demo_transforms_print :: () {
point := xy(1, 1);
t: Transform2D;
t.pos = xy(5, 0);
t.angle = degrees(45.0);
print("%\n", xform(t, point));
print("%\n", xform_inv(t, xform(t, point)));
}
demo_transforms :: () {
init();
t: Transform2D;
point := xy(2, 1);
while !quit {
frame_start();
pip(.{color = GREEN}, t.pos);
pip(.{}, xform(t, point));
POINTS :: 64;
cur_point := point;
cur_point_inv := point;
START_COLOR :: WHITE;
END_COLOR :: BLUE;
END_COLOR_INV :: ORANGE;
for 0..POINTS-1 {
progress: float = cast(float)it / cast(float)POINTS;
cur_point = xform(t, cur_point);
cur_point_inv = xform_inv(t, cur_point_inv);
pip(.{color = lerp(START_COLOR, END_COLOR, progress)}, cur_point);
pip(.{color = lerp(START_COLOR, END_COLOR_INV, progress)}, cur_point_inv);
}
print("\n");
horizontal: float = 0.0;
vertical: float = 0.0;
for Input.input_button_states if it & .DOWN {
keycode: Input.Key_Code = xx it_index;
if keycode == #char "D" horizontal += 1.0;
if keycode == #char "A" horizontal -= 1.0;
if keycode == #char "W" vertical += 1.0;
if keycode == #char "S" vertical -= 1.0;
if keycode == .MOUSE_BUTTON_RIGHT t.pos += mouse_delta_screen*camera.scale.x;
}
print("should be equal: % %\n", point, xform_inv(t, xform(t, point)));
t.angle += horizontal*dt*degrees(45.0);
t.scale.x += vertical*dt;
print("%\n", t);
frame_end();
}
}
demo_transforms2 :: () {
init();
draw_transform :: (d: DrawingSettings, t: Transform2D) {
horizontal := xform(t, xy(1.0, 0.0));
vertical := xform(t, xy(0.0, 1.0));
line(d, t.pos, vertical);
line(d, t.pos, horizontal);
pip(d, t.pos);
}
t1: Transform2D;
t2: Transform2D;
point := xy(2, 1);
while !quit {
print("%\n", camera);
frame_start();
defer frame_end();
combined := xform(t1, t2);
draw_transform(.{color = GREEN}, t1);
draw_transform(.{color = BLUE}, t2);
draw_transform(.{color = ORANGE}, combined);
pip(.{}, xform(t1, point));
pip(.{color = ORANGE}, xform(combined, point));
horizontal: float;
vertical: float;
right_panning: Vector2;
for Input.input_button_states if it & .DOWN {
keycode: Input.Key_Code = xx it_index;
if keycode == #char "D" horizontal += 1.0;
if keycode == #char "A" horizontal -= 1.0;
if keycode == #char "W" vertical += 1.0;
if keycode == #char "S" vertical -= 1.0;
if keycode == .MOUSE_BUTTON_RIGHT right_panning = mouse_delta_screen*camera.scale.x;
}
t1.angle += horizontal*dt*degrees(45.0);
t2.angle += vertical*dt*degrees(45.0);
t1.pos += right_panning;
//t1.scale.x += horizontal*dt;
t2.pos += 2.0*right_panning;
}
}
demo_contact :: () {
init();
size := Vector2.{1.0, 1.0};
from := Shape.{type = .Rectangle, halfsize = size/2.0};
to := Shape.{type = .Rectangle, offset = xy(0, 1.0), halfsize = size/2.0};
while !quit {
frame_start();
frame_end();
}
}
//main :: () { demo_circle(); }
//main :: () { demo_transforms_print(); }
//main :: () { demo_transforms(); }
main :: () { demo_transforms2(); }
//main :: () { demo_contact(); }

@ -1,240 +0,0 @@
#import "Window_Creation";
Debug :: #import "Debug";
Simp :: #import "Simp";
Input :: #import "Input";
#import "Math";
#import "Basic";
Camera :: struct {
pos: Vector2;
zoom: float = 1.0;
};
window: Window_Type;
quit: bool;
dt: float;
panning: bool = false;
camera : Camera;
mouse_frozen: bool;
mouse_frozen_at: Vector2;
last_time: float64;
last_mouse_pos: Vector2;
window_width : s32 = 1280;
window_height : s32 = 720;
dbgprint :: print; // so can be detected and removed
#scope_file;
normalize :: (using v: *Vector2) -> float {
sq := sqrt(x*x + y*y);
factor := 1.0 / sq;
x *= factor;
y *= factor;
return sq;
}
normalize :: (v: Vector2) -> Vector2 #must {
normalize(*v);
return v;
}
xy :: (x: int, y: int) -> Vector2 {
return xy(cast(float)x, cast(float)y);
}
#scope_export;
WHITE :: Vector4.{1.0, 1.0, 1.0, 1.0};
RED :: Vector4.{1.0, 0.0, 0.0, 1.0};
GREEN :: Vector4.{0.0, 1.0, 0.0, 1.0};
ORANGE :: Vector4.{1.0, 0xA5/255, 0.0, 1.0};
DrawingSettings :: struct {
world_space: bool = true;
color: Vector4 = WHITE;
};
line :: (using d: DrawingSettings, _from: Vector2, _to: Vector2, thickness: float = 1.0) {
width := thickness;
from := _from;
to := _to;
if world_space {
from = world_to_screen(from);
to = world_to_screen(to);
}
Simp.set_shader_for_color(true);
normal := rotate(unit_vector(to - from), PI/2.0);
Simp.immediate_quad(from + normal*width, from - normal*width, to + normal*width, to - normal*width, color = color);
}
vector :: (d: DrawingSettings, from: Vector2, vector: Vector2) {
line(d, from, from+vector);
arrow_head_length := length(vector) * 0.5;
// arrow head
line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, PI/2.0 + PI/4.0));
line(d, from + vector, from + vector + rotate(normalize(vector)*arrow_head_length, -PI/2.0 + -PI/4.0));
}
LastingPip :: struct {
alive_for: float;
pos: Vector2;
d: DrawingSettings;
};
pips : [10] LastingPip;
push_pip :: (d: DrawingSettings, at: Vector2) {
for * pips
{
if it.alive_for <= 0.0
{
it.alive_for = 1.0;
it.pos = at;
it.d = d;
break;
}
}
}
draw_pips :: (dt: float) {
for * pips if it.alive_for > 0.0
{
d := it.d;
d.color.w *= it.alive_for;
pip(d, it.pos);
it.alive_for -= dt;
}
}
pip :: (using d: DrawingSettings, _at: Vector2, size: float = 0.05) {
at := _at;
Simp.set_shader_for_color(true);
if world_space {
at = world_to_screen(at);
size /= camera.zoom;
}
Simp.immediate_quad(at + xy(-size, size), at + xy(size, size), at + xy(size, -size), at + xy(-size, -size), color = color);
}
font: *Simp.Dynamic_Font;
text :: (using d: DrawingSettings, t: string, p: Vector2) {
drawing_at := p;
if world_space {
drawing_at = world_to_screen(drawing_at);
}
Simp.draw_text(font, xx drawing_at.x, xx drawing_at.y, t, color = color);
}
init :: () {
window = create_window(window_width, window_height, "Test Kit");
// Actual render size in pixels can be different from the window dimensions we specified above (for example on high-resolution displays on macOS/iOS).
window_width, window_height = Simp.get_render_dimensions(window);
camera.pos = -xy(window_width, window_height)/2.0;
camera.zoom = 0.01;
Simp.set_render_target(window);
font = Simp.get_font_at_size(".", "Roboto-Regular.ttf", 18);
assert(font != null);
}
// screen coordinates
mouse_screen :: () -> Vector2 {
if mouse_frozen return mouse_frozen_at;
x, y := get_mouse_pointer_position();
pos: Vector2 = xy(cast(float)x, cast(float)y);
// simp is lower left is (0, 0) and y+ is up
pos.y = window_height - pos.y;
return pos;
}
mouse_world :: () -> Vector2 {
return screen_to_world(mouse_screen());
}
// query quit to see if should quit. Call this in a loop
frame_start :: () {
dt = cast(float)(get_time() - last_time);
last_time = get_time();
Input.update_window_events();
mouse_delta := mouse_screen() - last_mouse_pos; // using Input.mouse_delta_x seems to incorrectly accumulate
last_mouse_pos = mouse_screen();
for Input.get_window_resizes() {
Simp.update_window(it.window); // Simp will do nothing if it doesn't care about this window.
if it.window == window {
should_reinit := (it.width != window_width) || (it.height != window_height);
window_width = it.width;
window_height = it.height;
//if should_reinit my_init_fonts(); // Resize the font for the new window size.
}
}
camera.zoom *= 1.0 - 0.1*Input.mouse_delta_z/120.0;
if panning {
camera.pos -= mouse_delta;
}
Simp.immediate_begin();
Simp.clear_render_target(0.0, 0.0, 0.0, 1.0);
// draw grid
grid_d: DrawingSettings = .{true,.{0.2, 0.2, 0.2, 0.5}};
text_d := grid_d;
text_d.color = .{0.6, 0.6, 0.6, 1.0};
help_text_max :: 8;
help_zoom_max :: 0.04;
for x: -30..30 {
line(grid_d, xy(x, 30), xy(x, -30));
if camera.zoom <= help_zoom_max && abs(x) < help_text_max then text(text_d, tprint("%m", x), .{xx x, 0.0});
}
for y: -30..30 {
line(grid_d, xy(30, y), xy(-30, y));
if camera.zoom <= help_zoom_max && abs(y) < help_text_max then text(text_d, tprint("%m", y), .{0.0, xx y});
}
draw_pips(dt);
if mouse_frozen pip(.{false, ORANGE}, mouse_frozen_at, size = 20.0);
// handle inputs
for Input.events_this_frame {
if it.type == .QUIT then quit = true;
if it.type == {
case .KEYBOARD;
if it.key_pressed {
if it.key_code == {
case .ESCAPE;
quit = true;
case #char "T";
if !mouse_frozen mouse_frozen_at = mouse_screen();
mouse_frozen = !mouse_frozen;
}
}
if it.key_code == .MOUSE_BUTTON_LEFT {
panning = cast(bool)it.key_pressed;
}
}
}
}
frame_end :: () {
Simp.immediate_flush();
Simp.swap_buffers(window);
}
screen_to_world :: (screen: Vector2) -> Vector2 {
using camera;
return (screen + pos)*zoom;
}
world_to_screen :: (world: Vector2) -> Vector2 {
using camera;
// world = (screen + pos)*zoom;
// world/zoom = screen + pos;
// world/zoom - pos = screen;
return (world/zoom) - pos;
}
Loading…
Cancel
Save