diff --git a/render/world_to_screen/assets/SourceSansPro-Semibold.ttf b/render/world_to_screen/assets/SourceSansPro-Semibold.ttf new file mode 100644 index 0000000..5020594 Binary files /dev/null and b/render/world_to_screen/assets/SourceSansPro-Semibold.ttf differ diff --git a/render/world_to_screen/assets/materials/unlit.fp b/render/world_to_screen/assets/materials/unlit.fp new file mode 100644 index 0000000..7a76c3a --- /dev/null +++ b/render/world_to_screen/assets/materials/unlit.fp @@ -0,0 +1,27 @@ +#version 140 + +// Inputs should match the vertex shader's outputs. +in vec2 var_texcoord0; + +// The texture to sample. +uniform lowp sampler2D texture0; + +// The final color of the fragment. +out lowp vec4 final_color; + +uniform fs_uniforms +{ + mediump vec4 tint; +}; + +void main() +{ + // Pre-multiply alpha since all runtime textures already are + vec4 tint_pm = vec4(tint.xyz * tint.w, tint.w); + + // Sample the texture at the fragment's texture coordinates. + vec4 color = texture(texture0, var_texcoord0.xy) * tint_pm; + + // Output the sampled color. + final_color = color; +} diff --git a/render/world_to_screen/assets/materials/unlit.material b/render/world_to_screen/assets/materials/unlit.material new file mode 100644 index 0000000..d34a054 --- /dev/null +++ b/render/world_to_screen/assets/materials/unlit.material @@ -0,0 +1,31 @@ +name: "unlit" +tags: "model" +vertex_program: "/assets/materials/unlit.vp" +fragment_program: "/assets/materials/unlit.fp" +vertex_space: VERTEX_SPACE_LOCAL +vertex_constants { + name: "mtx_view" + type: CONSTANT_TYPE_VIEW +} +vertex_constants { + name: "mtx_proj" + type: CONSTANT_TYPE_PROJECTION +} +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +samplers { + name: "texture0" + wrap_u: WRAP_MODE_CLAMP_TO_EDGE + wrap_v: WRAP_MODE_CLAMP_TO_EDGE + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR + max_anisotropy: 0.0 +} diff --git a/render/world_to_screen/assets/materials/unlit.vp b/render/world_to_screen/assets/materials/unlit.vp new file mode 100644 index 0000000..f053d99 --- /dev/null +++ b/render/world_to_screen/assets/materials/unlit.vp @@ -0,0 +1,28 @@ +#version 140 + +// The model's vertex position and texture coordinates. +in vec4 position; +in vec2 texcoord0; + +// The model's world matrix. +in mat4 mtx_world; + +// The projection and view matrices. +uniform general_vp +{ + mat4 mtx_view; + mat4 mtx_proj; +}; + +// The output of a vertex shader are passed to the fragment shader. +// The texture coordinates of the vertex. +out vec2 var_texcoord0; + +void main() +{ + // Pass the texture coordinates to the fragment shader. + var_texcoord0 = texcoord0; + + // Transform the vertex position to clip space. + gl_Position = mtx_proj * mtx_view * mtx_world * vec4(position.xyz, 1.0); +} diff --git a/render/world_to_screen/assets/models/License.txt b/render/world_to_screen/assets/models/License.txt new file mode 100644 index 0000000..c12648f --- /dev/null +++ b/render/world_to_screen/assets/models/License.txt @@ -0,0 +1,28 @@ + + + Graveyard Kit (5.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 17-10-2025 11:00 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + You can use this content for personal, educational, and commercial purposes. + + Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement) + + ------------------------------ + + • Website : www.kenney.nl + • Donate : www.kenney.nl/donate + + • Patreon : patreon.com/kenney + + Follow on social media for updates: + + • Twitter: twitter.com/KenneyNL + • BlueSky: kenney.bsky.social + • Instagram: instagram.com/kenney_nl \ No newline at end of file diff --git a/render/world_to_screen/assets/models/character-ghost.glb b/render/world_to_screen/assets/models/character-ghost.glb new file mode 100644 index 0000000..8060798 Binary files /dev/null and b/render/world_to_screen/assets/models/character-ghost.glb differ diff --git a/render/world_to_screen/assets/models/colormap.png b/render/world_to_screen/assets/models/colormap.png new file mode 100644 index 0000000..5f61628 Binary files /dev/null and b/render/world_to_screen/assets/models/colormap.png differ diff --git a/render/world_to_screen/assets/models/crypt.glb b/render/world_to_screen/assets/models/crypt.glb new file mode 100644 index 0000000..a618fb7 Binary files /dev/null and b/render/world_to_screen/assets/models/crypt.glb differ diff --git a/render/world_to_screen/assets/text48.font b/render/world_to_screen/assets/text48.font new file mode 100644 index 0000000..2622c04 --- /dev/null +++ b/render/world_to_screen/assets/text48.font @@ -0,0 +1,5 @@ +font: "/assets/SourceSansPro-Semibold.ttf" +material: "/builtins/fonts/font.material" +size: 48 +outline_alpha: 0.0 +outline_width: 0.0 diff --git a/render/world_to_screen/example.md b/render/world_to_screen/example.md new file mode 100644 index 0000000..af4c3dd --- /dev/null +++ b/render/world_to_screen/example.md @@ -0,0 +1,15 @@ +--- +tags: render +title: World to Screen +brief: This example demonstrates how to convert 3D world coordinates to 2D screen coordinates using camera transformations. +author: Artsiom Trubchyk +scripts: player.script +thumbnail: thumbnail.png +--- + +This example shows how to convert world positions to screen coordinates for UI positioning. It features: + +* A `world_to_screen()` function that transforms 3D world positions to 2D screen coordinates using the camera's view and projection matrices. +* A ghost character that rotates around a crypt in 3D space while floating up and down. +* A player name label in the GUI that follows the character's world position by converting it to screen coordinates. +* Demonstrates practical use of world-to-screen conversion for positioning UI elements relative to 3D objects. diff --git a/render/world_to_screen/example/hud.gui b/render/world_to_screen/example/hud.gui new file mode 100644 index 0000000..dcaba71 --- /dev/null +++ b/render/world_to_screen/example/hud.gui @@ -0,0 +1,22 @@ +script: "/example/hud.gui_script" +fonts { + name: "text48" + font: "/assets/text48.font" +} +nodes { + scale { + x: 0.5 + y: 0.5 + } + size { + x: 400.0 + y: 100.0 + } + type: TYPE_TEXT + text: "PLAYER_NAME" + font: "text48" + id: "player_name" + inherit_alpha: true +} +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT diff --git a/render/world_to_screen/example/hud.gui_script b/render/world_to_screen/example/hud.gui_script new file mode 100644 index 0000000..116cafc --- /dev/null +++ b/render/world_to_screen/example/hud.gui_script @@ -0,0 +1,17 @@ +function init(self) + self.name_node = gui.get_node("player_name") +end + +function final(self) +end + +function update(self, dt) +end + +function on_message(self, message_id, message, sender) + if message_id == hash("update_data") then + local screen_position = message.screen_position + -- Use screen position to set the position of the player name node + gui.set_screen_position(self.name_node, screen_position) + end +end diff --git a/render/world_to_screen/example/player.script b/render/world_to_screen/example/player.script new file mode 100644 index 0000000..e382166 --- /dev/null +++ b/render/world_to_screen/example/player.script @@ -0,0 +1,60 @@ +go.property("camera_url", msg.url("/camera#camera")) +go.property("hud_url", msg.url("/ui#hud")) +go.property("angle", -45) -- we use this property to animate the rotation of the player around the center of the scene + +--- Converts a world position to screen coordinates. +-- This function transforms a 3D world position to 2D screen coordinates using the camera's +-- view and projection matrices. The resulting coordinates are in screen space where (0,0) +-- is the bottom-left corner of the screen. +-- +-- @param world_position vector3 The world position to convert. +-- @param camera_url url|string The camera component URL to use for the transformation. +-- @return number screen_x The X coordinate in screen space. +-- @return number screen_y The Y coordinate in screen space. +-- @return number screen_z Always returns 0 (depth information is not preserved). +local function world_to_screen(world_position, camera_url) + local proj = camera.get_projection(camera_url) + local view = camera.get_view(camera_url) + + local view_proj = proj * view + local scr_coord = view_proj * vmath.vector4(world_position.x, world_position.y, world_position.z, 1) + local w, h = window.get_size() + scr_coord.x = (scr_coord.x / scr_coord.w + 1) * 0.5 * w + scr_coord.y = (scr_coord.y / scr_coord.w + 1) * 0.5 * h + + return vmath.vector3(scr_coord.x, scr_coord.y, 0) +end + +function init(self) + -- Get the IDs of the player view and UI objects + self.player_view_id = go.get_id("player_view") + self.player_ui_id = go.get_id("player_ui") + + -- Animate vertical position of the body + local new_pos_y = go.get(self.player_view_id, "position.y") + 0.2 + go.animate(self.player_view_id, "position.y", go.PLAYBACK_LOOP_PINGPONG, new_pos_y, go.EASING_INOUTSINE, 2) + + -- Get the base position + self.base_pos = go.get_position() + + -- Animate the angle to rotate the player around the center of the scene + go.animate("#", "angle", go.PLAYBACK_LOOP_FORWARD, -3600 + self.angle, go.EASING_LINEAR, 200) +end + +function final(self) +end + +function update(self, dt) + -- Update the position of the player based on the angle and the base position + local radius = self.base_pos.z + go.set_position(vmath.vector3(radius * math.sin(math.rad(self.angle)), self.base_pos.y, radius * math.cos(math.rad(self.angle)))) + -- Update the rotation of the player based on the angle + go.set(".", "euler.y", self.angle + 90) + + -- Update the world transform of the player UI object and convert the world position to screen coordinates + go.update_world_transform(self.player_ui_id) + local world_pos = go.get_world_position(self.player_ui_id) + local screen_pos = world_to_screen(world_pos, self.camera_url) + -- Send the screen position to the HUD script + msg.post(self.hud_url, "update_data", { screen_position = screen_pos }) +end diff --git a/render/world_to_screen/example/world_to_screen.collection b/render/world_to_screen/example/world_to_screen.collection new file mode 100644 index 0000000..d16f769 --- /dev/null +++ b/render/world_to_screen/example/world_to_screen.collection @@ -0,0 +1,146 @@ +name: "world_to_screen" +scale_along_z: 0 +embedded_instances { + id: "camera" + data: "embedded_components {\n" + " id: \"camera\"\n" + " type: \"camera\"\n" + " data: \"aspect_ratio: 1.0\\n" + "fov: 0.7854\\n" + "near_z: 0.01\\n" + "far_z: 100.0\\n" + "orthographic_mode: ORTHO_MODE_AUTO_FIT\\n" + "\"\n" + "}\n" + "" + position { + y: 5.0 + z: 5.0 + } + rotation { + x: -0.37036422 + w: 0.9288866 + } +} +embedded_instances { + id: "player" + children: "player_shadow" + children: "player_ui" + children: "player_view" + data: "components {\n" + " id: \"player\"\n" + " component: \"/example/player.script\"\n" + "}\n" + "" + position { + y: 0.3 + z: -2.0 + } +} +embedded_instances { + id: "ui" + data: "components {\n" + " id: \"hud\"\n" + " component: \"/example/hud.gui\"\n" + "}\n" + "" + position { + z: -361.0 + } +} +embedded_instances { + id: "environment" + data: "embedded_components {\n" + " id: \"crypt\"\n" + " type: \"model\"\n" + " data: \"mesh: \\\"/assets/models/crypt.glb\\\"\\n" + "name: \\\"{{NAME}}\\\"\\n" + "materials {\\n" + " name: \\\"colormap\\\"\\n" + " material: \\\"/assets/materials/unlit.material\\\"\\n" + " textures {\\n" + " sampler: \\\"texture0\\\"\\n" + " texture: \\\"/assets/models/colormap.png\\\"\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "embedded_components {\n" + " id: \"crypt_shadow\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"anim\\\"\\n" + "material: \\\"/builtins/materials/sprite.material\\\"\\n" + "size {\\n" + " x: 2.5\\n" + " y: 3.5\\n" + "}\\n" + "size_mode: SIZE_MODE_MANUAL\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/builtins/graphics/particle_blob.tilesource\\\"\\n" + "}\\n" + "\"\n" + " rotation {\n" + " x: 0.70710677\n" + " w: 0.70710677\n" + " }\n" + "}\n" + "" + rotation { + y: 0.70710677 + w: 0.70710677 + } +} +embedded_instances { + id: "player_view" + data: "embedded_components {\n" + " id: \"model\"\n" + " type: \"model\"\n" + " data: \"mesh: \\\"/assets/models/character-ghost.glb\\\"\\n" + "name: \\\"{{NAME}}\\\"\\n" + "materials {\\n" + " name: \\\"colormap\\\"\\n" + " material: \\\"/assets/materials/unlit.material\\\"\\n" + " textures {\\n" + " sampler: \\\"texture0\\\"\\n" + " texture: \\\"/assets/models/colormap.png\\\"\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" +} +embedded_instances { + id: "player_shadow" + data: "embedded_components {\n" + " id: \"sprite\"\n" + " type: \"sprite\"\n" + " data: \"default_animation: \\\"anim\\\"\\n" + "material: \\\"/builtins/materials/sprite.material\\\"\\n" + "size {\\n" + " x: 1.2\\n" + " y: 1.2\\n" + "}\\n" + "size_mode: SIZE_MODE_MANUAL\\n" + "textures {\\n" + " sampler: \\\"texture_sampler\\\"\\n" + " texture: \\\"/builtins/graphics/particle_blob.tilesource\\\"\\n" + "}\\n" + "\"\n" + " rotation {\n" + " x: 0.70710677\n" + " w: 0.70710677\n" + " }\n" + "}\n" + "" + position { + y: -0.3 + } +} +embedded_instances { + id: "player_ui" + data: "" + position { + y: 1.0 + } +} diff --git a/render/world_to_screen/game.project b/render/world_to_screen/game.project new file mode 100644 index 0000000..d7d0162 --- /dev/null +++ b/render/world_to_screen/game.project @@ -0,0 +1,62 @@ +[project] +title = render-world_to_screen +version = 0.1 + +[bootstrap] +main_collection = /example/world_to_screen.collectionc + +[input] +game_binding = /builtins/input/all.input_bindingc +repeat_interval = 0.05 + +[display] +width = 720 +height = 720 +high_dpi = 1 + +[physics] +scale = 0.02 +gravity_y = -500.0 + +[script] +shared_state = 1 + +[collection_proxy] +max_count = 256 + +[label] +subpixels = 1 + +[sprite] +subpixels = 1 +max_count = 32765 + +[windows] +iap_provider = + +[android] +package = com.defold.examples + +[ios] +bundle_identifier = com.defold.examples + +[osx] +bundle_identifier = com.defold.examples + +[html5] +show_fullscreen_button = 0 +show_made_with_defold = 0 +scale_mode = no_scale +heap_size = 64 + +[collection] +max_instances = 32765 + +[particle_fx] +max_emitter_count = 1024 + +[render] +clear_color_blue = 0.1 +clear_color_green = 0.1 +clear_color_red = 0.1 + diff --git a/render/world_to_screen/thumbnail.png b/render/world_to_screen/thumbnail.png new file mode 100644 index 0000000..b28f0c0 Binary files /dev/null and b/render/world_to_screen/thumbnail.png differ