diff --git a/movement/look_at_3d/.gitattributes b/movement/look_at_3d/.gitattributes new file mode 100644 index 00000000..187cdf4a --- /dev/null +++ b/movement/look_at_3d/.gitattributes @@ -0,0 +1,42 @@ +# Defold Protocol Buffer Text Files (https://github.com/github/linguist/issues/5091) +*.animationset linguist-language=JSON5 +*.atlas linguist-language=JSON5 +*.camera linguist-language=JSON5 +*.collection linguist-language=JSON5 +*.collectionfactory linguist-language=JSON5 +*.collectionproxy linguist-language=JSON5 +*.collisionobject linguist-language=JSON5 +*.cubemap linguist-language=JSON5 +*.display_profiles linguist-language=JSON5 +*.factory linguist-language=JSON5 +*.font linguist-language=JSON5 +*.gamepads linguist-language=JSON5 +*.go linguist-language=JSON5 +*.gui linguist-language=JSON5 +*.input_binding linguist-language=JSON5 +*.label linguist-language=JSON5 +*.material linguist-language=JSON5 +*.mesh linguist-language=JSON5 +*.model linguist-language=JSON5 +*.particlefx linguist-language=JSON5 +*.render linguist-language=JSON5 +*.sound linguist-language=JSON5 +*.sprite linguist-language=JSON5 +*.spinemodel linguist-language=JSON5 +*.spinescene linguist-language=JSON5 +*.texture_profiles linguist-language=JSON5 +*.tilemap linguist-language=JSON5 +*.tilesource linguist-language=JSON5 + +# Defold JSON Files +*.buffer linguist-language=JSON + +# Defold GLSL Shaders +*.fp linguist-language=GLSL +*.vp linguist-language=GLSL + +# Defold Lua Files +*.editor_script linguist-language=Lua +*.render_script linguist-language=Lua +*.script linguist-language=Lua +*.gui_script linguist-language=Lua diff --git a/movement/look_at_3d/.gitignore b/movement/look_at_3d/.gitignore new file mode 100644 index 00000000..0f4d6130 --- /dev/null +++ b/movement/look_at_3d/.gitignore @@ -0,0 +1,11 @@ +/.editor_settings +/.internal +/build +.externalToolBuilders +.DS_Store +Thumbs.db +.lock-wscript +*.pyc +.project +.cproject +builtins \ No newline at end of file diff --git a/movement/look_at_3d/example.md b/movement/look_at_3d/example.md new file mode 100644 index 00000000..8bf74dc8 --- /dev/null +++ b/movement/look_at_3d/example.md @@ -0,0 +1,81 @@ +--- +title: Rotate 3D object towards the mouse cursor +brief: Learn how to rotate a 3D object towards the mouse cursor. +author_ids: + - evgenii-starostin +scripts: turret_controller.script, camera_controller.script +thumbnail: thumbnail.webp +tags: movement +--- +This example demonstrates how to rotate a 3D object towards the mouse cursor. + +## Project Structure +``` +- camera.go + - camera + - camera_controller.script + floor.go + - floor + - floor_uv_scale.script + root.go + - turret.go + - turret + - turret_controller.script + - base +``` + +## How It Works +First, the script that controls the rotation needs a reference to the active camera. This is done by sending a message in `init`: + +```lua + msg.post("/turret#turret_controller", "register_camera", { + url = msg.url("#camera") + }) +``` + +In turret_controller.script we use the camera to build a picking ray - a line that starts at the camera and goes through the cursor position in the world: + +```lua +-- z = 0 -> near plane, z = 1 -> far plane (normalized depth) +local p0 = camera.screen_to_world( + vmath.vector3(self.mouse_x, self.mouse_y, 0), + self.camera_url +) + +local p1 = camera.screen_to_world( + vmath.vector3(self.mouse_x, self.mouse_y, 1), + self.camera_url +) + +local ray = p1 - p0 +``` + +Then we intersect this ray with the horizontal plane at the turret's height (y = gun_pos.y): +```lua +go.update_world_transform() +local gun_pos = go.get_world_position() + +-- avoid division by zero when the ray is parallel to the plane +if math.abs(ray.y) < 0.00001 then + return +end + +local t = (gun_pos.y - p0.y) / ray.y +local target = p0 + ray * t +``` + +Finally, we compute the yaw angle from the turret to the target and rotate the object around the Y axis. atan2(dx, dz) assumes the model faces +Z at angle 0: +```lua +local dx = target.x - gun_pos.x +local dz = target.z - gun_pos.z + +if math.abs(dx) + math.abs(dz) < 0.00001 then + return +end + +local angle = math.atan2(dx, dz) + +go.set_rotation( + vmath.quat_rotation_y(angle) +) +``` \ No newline at end of file diff --git a/movement/look_at_3d/game.project b/movement/look_at_3d/game.project new file mode 100644 index 00000000..5d522acd --- /dev/null +++ b/movement/look_at_3d/game.project @@ -0,0 +1,26 @@ +[bootstrap] +main_collection = /main/main.collectionc + +[script] +shared_state = 1 + +[display] +width = 720 +height = 720 +high_dpi = 1 + +[android] +input_method = HiddenInputField + +[physics] +gravity_y = -1000.0 +scale = 0.01 + +[html5] +scale_mode = no_scale + +[project] +title = Look at 3D +dependencies = +developer = + diff --git a/movement/look_at_3d/input/game.input_binding b/movement/look_at_3d/input/game.input_binding new file mode 100644 index 00000000..8ed1d4e4 --- /dev/null +++ b/movement/look_at_3d/input/game.input_binding @@ -0,0 +1,4 @@ +mouse_trigger { + input: MOUSE_BUTTON_1 + action: "touch" +} diff --git a/movement/look_at_3d/main/camera_controller.script b/movement/look_at_3d/main/camera_controller.script new file mode 100644 index 00000000..825ede18 --- /dev/null +++ b/movement/look_at_3d/main/camera_controller.script @@ -0,0 +1,5 @@ +function init(self) + msg.post("/turret#turret_controller", "register_camera", { + url = msg.url("#camera") + }) +end diff --git a/movement/look_at_3d/main/floor_uv_scale.script b/movement/look_at_3d/main/floor_uv_scale.script new file mode 100644 index 00000000..2d8b2e49 --- /dev/null +++ b/movement/look_at_3d/main/floor_uv_scale.script @@ -0,0 +1,3 @@ +function init(self) + go.set("#floor", "uv_scale", vmath.vector4(10, 10, 0, 0)) +end diff --git a/movement/look_at_3d/main/images/colormap.png b/movement/look_at_3d/main/images/colormap.png new file mode 100644 index 00000000..7873e94f Binary files /dev/null and b/movement/look_at_3d/main/images/colormap.png differ diff --git a/movement/look_at_3d/main/images/texture_02.png b/movement/look_at_3d/main/images/texture_02.png new file mode 100644 index 00000000..19aad62c Binary files /dev/null and b/movement/look_at_3d/main/images/texture_02.png differ diff --git a/movement/look_at_3d/main/main.collection b/movement/look_at_3d/main/main.collection new file mode 100644 index 00000000..0a7446b2 --- /dev/null +++ b/movement/look_at_3d/main/main.collection @@ -0,0 +1,107 @@ +name: "main" +scale_along_z: 0 +embedded_instances { + id: "root" + children: "turret" + data: "embedded_components {\n" + " id: \"base\"\n" + " type: \"model\"\n" + " data: \"mesh: \\\"/main/models/base.glb\\\"\\n" + "skeleton: \\\"/main/models/base.glb\\\"\\n" + "animations: \\\"/main/models/base.glb\\\"\\n" + "name: \\\"{{NAME}}\\\"\\n" + "materials {\\n" + " name: \\\"default\\\"\\n" + " material: \\\"/main/materials/static/unlit.material\\\"\\n" + " textures {\\n" + " sampler: \\\"tex0\\\"\\n" + " texture: \\\"/main/images/colormap.png\\\"\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" +} +embedded_instances { + id: "camera" + data: "components {\n" + " id: \"camera_controller\"\n" + " component: \"/main/camera_controller.script\"\n" + "}\n" + "embedded_components {\n" + " id: \"camera\"\n" + " type: \"camera\"\n" + " data: \"aspect_ratio: 1.0\\n" + "fov: 1.047\\n" + "near_z: 2.1\\n" + "far_z: 30.0\\n" + "auto_aspect_ratio: 1\\n" + "orthographic_zoom: 116.0\\n" + "\"\n" + "}\n" + "" + position { + y: 5.0 + z: 6.0 + } + rotation { + x: -0.34202015 + w: 0.9396926 + } +} +embedded_instances { + id: "turret" + data: "components {\n" + " id: \"turret_controller\"\n" + " component: \"/main/turret_controller.script\"\n" + "}\n" + "embedded_components {\n" + " id: \"turret\"\n" + " type: \"model\"\n" + " data: \"mesh: \\\"/main/models/turret.glb\\\"\\n" + "skeleton: \\\"/main/models/turret.glb\\\"\\n" + "animations: \\\"/main/models/turret.glb\\\"\\n" + "name: \\\"{{NAME}}\\\"\\n" + "materials {\\n" + " name: \\\"default\\\"\\n" + " material: \\\"/main/materials/skinned/unlit_skinned.material\\\"\\n" + " textures {\\n" + " sampler: \\\"tex0\\\"\\n" + " texture: \\\"/main/images/colormap.png\\\"\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" +} +embedded_instances { + id: "floor" + data: "components {\n" + " id: \"floor_uv_scale\"\n" + " component: \"/main/floor_uv_scale.script\"\n" + "}\n" + "embedded_components {\n" + " id: \"floor\"\n" + " type: \"model\"\n" + " data: \"mesh: \\\"/builtins/assets/gltf/quad.gltf\\\"\\n" + "name: \\\"{{NAME}}\\\"\\n" + "materials {\\n" + " name: \\\"default\\\"\\n" + " material: \\\"/main/materials/static/unlit.material\\\"\\n" + " textures {\\n" + " sampler: \\\"tex0\\\"\\n" + " texture: \\\"/main/images/texture_02.png\\\"\\n" + " }\\n" + "}\\n" + "\"\n" + "}\n" + "" + rotation { + x: -0.70710677 + w: 0.70710677 + } + scale3 { + x: 100.0 + y: 100.0 + } +} diff --git a/movement/look_at_3d/main/materials/skinned/unlit_skinned.fp b/movement/look_at_3d/main/materials/skinned/unlit_skinned.fp new file mode 100644 index 00000000..a70f9994 --- /dev/null +++ b/movement/look_at_3d/main/materials/skinned/unlit_skinned.fp @@ -0,0 +1,16 @@ +#version 140 + +in vec2 var_texcoord0; +uniform mediump sampler2D tex0; +out lowp vec4 final_color; +uniform fs_uniforms +{ + mediump vec4 tint; +}; + +void main() +{ + vec4 tint_pm = vec4(tint.xyz * tint.w, tint.w); + vec4 color = texture(tex0, var_texcoord0.xy) * tint_pm; + final_color = color; +} \ No newline at end of file diff --git a/movement/look_at_3d/main/materials/skinned/unlit_skinned.material b/movement/look_at_3d/main/materials/skinned/unlit_skinned.material new file mode 100644 index 00000000..97ccdf60 --- /dev/null +++ b/movement/look_at_3d/main/materials/skinned/unlit_skinned.material @@ -0,0 +1,30 @@ +name: "unlit" +tags: "model" +vertex_program: "/main/materials/skinned/unlit_skinned.vp" +fragment_program: "/main/materials/skinned/unlit_skinned.fp" +vertex_space: VERTEX_SPACE_LOCAL +vertex_constants { + name: "mtx_worldview" + type: CONSTANT_TYPE_WORLDVIEW +} +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: "tex0" + 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 +} diff --git a/movement/look_at_3d/main/materials/skinned/unlit_skinned.vp b/movement/look_at_3d/main/materials/skinned/unlit_skinned.vp new file mode 100644 index 00000000..8d408be2 --- /dev/null +++ b/movement/look_at_3d/main/materials/skinned/unlit_skinned.vp @@ -0,0 +1,27 @@ +#version 140 + +in highp vec4 position; +in mediump vec2 texcoord0; +in mediump vec4 bone_weights; +in mediump vec4 bone_indices; + +out mediump vec2 var_texcoord0; + +uniform vs_uniforms +{ + mediump mat4 mtx_worldview; + mediump mat4 mtx_proj; + mediump vec4 animation_data; +}; + +#include "/builtins/materials/skinning.glsl" + +void main() +{ + vec4 skinned_position = get_skinned_position(position); + + var_texcoord0 = texcoord0; + + vec4 p = mtx_worldview * vec4(skinned_position.xyz, 1.0); + gl_Position = mtx_proj * p; +} \ No newline at end of file diff --git a/movement/look_at_3d/main/materials/static/unlit.fp b/movement/look_at_3d/main/materials/static/unlit.fp new file mode 100644 index 00000000..deaf4bbe --- /dev/null +++ b/movement/look_at_3d/main/materials/static/unlit.fp @@ -0,0 +1,25 @@ +#version 140 + + +in mediump vec2 var_texcoord0; +uniform mediump sampler2D tex0; +in highp mat4 var_view; + +out vec4 out_fragColor; + +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(tex0, var_texcoord0.xy) * tint_pm; + + // Output the sampled color. + out_fragColor = color; +} \ No newline at end of file diff --git a/movement/look_at_3d/main/materials/static/unlit.material b/movement/look_at_3d/main/materials/static/unlit.material new file mode 100644 index 00000000..ebcfbd02 --- /dev/null +++ b/movement/look_at_3d/main/materials/static/unlit.material @@ -0,0 +1,40 @@ +name: "unlit" +tags: "model" +vertex_program: "/main/materials/static/unlit.vp" +fragment_program: "/main/materials/static/unlit.fp" +vertex_space: VERTEX_SPACE_LOCAL +vertex_constants { + name: "mtx_worldview" + type: CONSTANT_TYPE_WORLDVIEW +} +vertex_constants { + name: "mtx_proj" + type: CONSTANT_TYPE_PROJECTION +} +vertex_constants { + name: "uv_scale" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +fragment_constants { + name: "tint" + type: CONSTANT_TYPE_USER + value { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } +} +samplers { + name: "tex0" + wrap_u: WRAP_MODE_REPEAT + wrap_v: WRAP_MODE_REPEAT + filter_min: FILTER_MODE_MIN_LINEAR + filter_mag: FILTER_MODE_MAG_LINEAR +} diff --git a/movement/look_at_3d/main/materials/static/unlit.vp b/movement/look_at_3d/main/materials/static/unlit.vp new file mode 100644 index 00000000..ab7c126d --- /dev/null +++ b/movement/look_at_3d/main/materials/static/unlit.vp @@ -0,0 +1,21 @@ +#version 140 + +in highp vec4 position; +in mediump vec2 texcoord0; + +out mediump vec2 var_texcoord0; + +uniform vs_uniforms +{ + mediump mat4 mtx_worldview; + mediump mat4 mtx_proj; + mediump vec4 uv_scale; +}; + +void main() +{ + var_texcoord0 = texcoord0 * uv_scale.xy; + + vec4 p = mtx_worldview * vec4(position.xyz, 1.0); + gl_Position = mtx_proj * p; +} \ No newline at end of file diff --git a/movement/look_at_3d/main/models/base.glb b/movement/look_at_3d/main/models/base.glb new file mode 100644 index 00000000..4975dd66 Binary files /dev/null and b/movement/look_at_3d/main/models/base.glb differ diff --git a/movement/look_at_3d/main/models/turret.glb b/movement/look_at_3d/main/models/turret.glb new file mode 100644 index 00000000..1f64e17e Binary files /dev/null and b/movement/look_at_3d/main/models/turret.glb differ diff --git a/movement/look_at_3d/main/turret_controller.script b/movement/look_at_3d/main/turret_controller.script new file mode 100644 index 00000000..a60b6fd3 --- /dev/null +++ b/movement/look_at_3d/main/turret_controller.script @@ -0,0 +1,83 @@ +local MODEL_URL = "#turret" +local TOUCH = hash("touch") +local SHOOT = hash("shoot") + +local function play_animation(animation_id) + model.play_anim(MODEL_URL, animation_id, go.PLAYBACK_ONCE_FORWARD) +end + +function init(self) + msg.post(".", "acquire_input_focus") + + self.camera_url = nil + self.mouse_x = 0 + self.mouse_y = 0 + self.has_mouse = false +end + +function on_message(self, message_id, message, sender) + if message_id == hash("register_camera") then + self.camera_url = message.url + pprint("Camera registered: " .. tostring(self.camera_url)) + end +end + +function on_input(self, action_id, action) + if action.screen_x and action.screen_y then + self.mouse_x = action.screen_x + self.mouse_y = action.screen_y + self.has_mouse = true + if action_id == TOUCH and action.pressed then + play_animation(SHOOT) + end + end +end + +function update(self, dt) + if not self.camera_url or not self.has_mouse then + return + end + + -- Get two points on the cursor's line of sight (near and far planes) + -- z = 0 -> near plane, z = 1 -> far plane (normalized depth, not world units) + local p0 = camera.screen_to_world( + vmath.vector3(self.mouse_x, self.mouse_y, 0), + self.camera_url + ) + + local p1 = camera.screen_to_world( + vmath.vector3(self.mouse_x, self.mouse_y, 1), + self.camera_url + ) + -- Ray direction: from near plane towards far plane + local ray = p1 - p0 + + -- Get the turret's current world position (with parent transforms applied) + go.update_world_transform() + local gun_pos = go.get_world_position() + + -- Ray is parallel to the horizontal plane - no intersection + if math.abs(ray.y) < 0.00001 then + return + end + -- Intersect the ray with the horizontal plane at the gun's height (y = gun_pos.y) + local t = (gun_pos.y - p0.y) / ray.y + local target = p0 + ray * t + + -- Horizontal vector from turret to target + local dx = target.x - gun_pos.x + local dz = target.z - gun_pos.z + + -- Target is directly under/above the turret - direction is undefined + if math.abs(dx) + math.abs(dz) < 0.00001 then + return + end + + -- Yaw angle. atan2(dx, dz) assumes the model faces +Z at angle 0 + local angle = math.atan2(dx, dz) + + -- Rotate the turret around the Y axis + go.set_rotation( + vmath.quat_rotation_y(angle) + ) +end diff --git a/movement/look_at_3d/thumbnail.webp b/movement/look_at_3d/thumbnail.webp new file mode 100644 index 00000000..79411585 Binary files /dev/null and b/movement/look_at_3d/thumbnail.webp differ