Explorar o código

Add entity picking example (#90)

Artsiom Trubchyk hai 3 meses
pai
achega
88bfb413bd

+ 18 - 0
input/entity_picking/all.texture_profiles

@@ -0,0 +1,18 @@
+path_settings {
+  path: "**"
+  profile: "Default"
+}
+profiles {
+  name: "Default"
+  platforms {
+    os: OS_ID_GENERIC
+    formats {
+      format: TEXTURE_FORMAT_RGBA
+      compression_level: BEST
+      compression_type: COMPRESSION_TYPE_DEFAULT
+    }
+    mipmaps: false
+    max_texture_size: 0
+    premultiply_alpha: true
+  }
+}

+ 27 - 0
input/entity_picking/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;
+}

+ 31 - 0
input/entity_picking/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
+}

+ 28 - 0
input/entity_picking/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);
+}

+ 28 - 0
input/entity_picking/assets/models/kenney_prototype-kit/License.txt

@@ -0,0 +1,28 @@
+	
+
+	Prototype Kit (1.0)
+
+	Created/distributed by Kenney (www.kenney.nl)
+	Creation date: 28-08-2024 09:59
+	
+			------------------------------
+
+	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
+	• Instagram: instagram.com/kenney_nl
+	• Mastodon:  mastodon.gamedev.place/@kenney

BIN=BIN
input/entity_picking/assets/models/kenney_prototype-kit/Textures/colormap.png


BIN=BIN
input/entity_picking/assets/models/kenney_prototype-kit/coin.glb


BIN=BIN
input/entity_picking/assets/models/kenney_prototype-kit/target-a-round.glb


BIN=BIN
input/entity_picking/assets/models/kenney_prototype-kit/target-a-square.glb


+ 15 - 0
input/entity_picking/example.md

@@ -0,0 +1,15 @@
+---
+tags: input
+title: Entity Picking
+brief: This example demonstrates how to pick a game object from the 3D scene.
+author: Artsiom Trubchyk
+scripts: entity_picking.script
+---
+
+This example describes method of selecting a game object from the 3D scene on the click of the mouse using collision-based picking:
+
+* We use [collision object components](https://defold.com/manuals/physics-objects/) to define a pickable shape for each relevant game object. This example uses 3D physics, which is enabled in the `game.project` file.
+* When the user clicks the mouse button, we convert screen coordinates to world coordinates and fire a raycast into the 3D world using the `physics.raycast()` function.
+* If the ray intersects with a collision object, the corresponding game object is considered "picked".
+
+The models used in this example are from Kenney's [Prototype Kit](https://kenney.nl/assets/prototype-kit), licensed under CC0.

+ 42 - 0
input/entity_picking/example/camera_math.lua

@@ -0,0 +1,42 @@
+local M = {}
+
+--- Convert a point from 2D screen space to 3D world space. Supports only perspective.
+-- @param x number X coordinate on screen.
+-- @param y number Y coordinate on screen.
+-- @param z number The distance from the camera in world space to create the new point.
+-- @param camera_id url The camera URL to get params from.
+-- @return vector3 The world coordinate.
+function M.screen_to_world(x, y, z, camera_id)
+	-- Camera properties
+	local projection = camera.get_projection(camera_id)
+	assert(projection.m33 == 0.0, "Camera must be in perspective mode")
+
+	local cw, ch = window.get_size()
+	local aspect_ratio = cw / ch
+	local near_z = camera.get_near_z(camera_id)
+	local fov = camera.get_fov(camera_id)
+	local inv_view = vmath.inv(camera.get_view(camera_id))
+
+	-- Calculate the screen click as a point on the far plane of the normalized device coordinate 'box' (z=1)
+	local ndc_x = x / cw * 2 - 1
+	local ndc_y = y / ch * 2 - 1
+
+	-- Calculate perspective projection matrix half size at the near plane
+	local half_size = vmath.vector4(0, 0, -near_z, 1)
+	local h = near_z * math.tan(fov / 2)
+	half_size.x = h * aspect_ratio * ndc_x
+	half_size.y = h * ndc_y
+
+	-- Transform to world space
+	local point = inv_view * half_size
+
+	-- Move to distance z from the camera
+	local world_coord = vmath.normalize(vmath.vector3(point.x - inv_view.m03, point.y - inv_view.m13, point.z - inv_view.m23))
+	world_coord.x = world_coord.x * z + inv_view.m03
+	world_coord.y = world_coord.y * z + inv_view.m13
+	world_coord.z = world_coord.z * z + inv_view.m23
+
+	return world_coord
+end
+
+return M

+ 23 - 0
input/entity_picking/example/coin.go

@@ -0,0 +1,23 @@
+components {
+  id: "coin"
+  component: "/example/coin.script"
+}
+embedded_components {
+  id: "model"
+  type: "model"
+  data: "mesh: \"/assets/models/kenney_prototype-kit/coin.glb\"\n"
+  "name: \"{{NAME}}\"\n"
+  "materials {\n"
+  "  name: \"colormap\"\n"
+  "  material: \"/assets/materials/unlit.material\"\n"
+  "  textures {\n"
+  "    sampler: \"texture0\"\n"
+  "    texture: \"/assets/models/kenney_prototype-kit/Textures/colormap.png\"\n"
+  "  }\n"
+  "}\n"
+  ""
+  rotation {
+    y: 0.70710677
+    w: 0.70710677
+  }
+}

+ 7 - 0
input/entity_picking/example/coin.script

@@ -0,0 +1,7 @@
+function init(self)
+	local start_angle = go.get(".", "euler.y")
+	local to_angle = start_angle - 3600
+
+	-- Simply animate ...
+	go.animate(".", "euler.y", go.PLAYBACK_LOOP_FORWARD, to_angle, go.EASING_LINEAR, 8)
+end

+ 227 - 0
input/entity_picking/example/entity_picking.collection

@@ -0,0 +1,227 @@
+name: "gltf"
+instances {
+  id: "target17"
+  prototype: "/example/target_square.go"
+  position {
+    x: -0.786375
+    y: 0.886167
+  }
+}
+instances {
+  id: "target1"
+  prototype: "/example/target_round.go"
+  position {
+    x: -1.01983
+    y: -0.513669
+  }
+}
+instances {
+  id: "target2"
+  prototype: "/example/target_round.go"
+  position {
+    x: 0.626642
+    y: 0.509664
+  }
+}
+instances {
+  id: "target3"
+  prototype: "/example/target_square.go"
+  position {
+    x: 0.491484
+    y: -1.008233
+  }
+}
+instances {
+  id: "target4"
+  prototype: "/example/target_round.go"
+  position {
+    x: 0.712652
+    y: 1.984537
+  }
+}
+instances {
+  id: "target5"
+  prototype: "/example/target_round.go"
+  position {
+    x: -0.737226
+    y: 2.659231
+  }
+}
+instances {
+  id: "target6"
+  prototype: "/example/target_round.go"
+  position {
+    x: 2.355764
+    y: 1.031183
+  }
+}
+instances {
+  id: "target7"
+  prototype: "/example/target_round.go"
+  position {
+    x: -2.329206
+    y: 0.833912
+  }
+}
+instances {
+  id: "target8"
+  prototype: "/example/target_round.go"
+  position {
+    x: 2.151081
+    y: -1.252797
+  }
+}
+instances {
+  id: "target9"
+  prototype: "/example/target_round.go"
+  position {
+    x: -2.260979
+    y: -1.445039
+  }
+}
+instances {
+  id: "target10"
+  prototype: "/example/target_square.go"
+  position {
+    x: 3.434709
+    y: 2.304017
+  }
+}
+instances {
+  id: "target11"
+  prototype: "/example/target_round.go"
+  position {
+    x: 4.395792
+    y: -1.316082
+  }
+}
+instances {
+  id: "target12"
+  prototype: "/example/target_round.go"
+  position {
+    x: 2.700948
+    y: -3.094295
+  }
+}
+instances {
+  id: "target13"
+  prototype: "/example/target_square.go"
+  position {
+    x: -1.656999
+    y: -3.453754
+  }
+}
+instances {
+  id: "target14"
+  prototype: "/example/target_round.go"
+  position {
+    x: -4.902927
+    y: -0.680336
+  }
+}
+instances {
+  id: "target15"
+  prototype: "/example/target_square.go"
+  position {
+    x: -4.167576
+    y: 1.297699
+  }
+}
+instances {
+  id: "target16"
+  prototype: "/example/target_round.go"
+  position {
+    x: -5.324439
+    y: 2.883688
+  }
+}
+instances {
+  id: "target18"
+  prototype: "/example/target_round.go"
+  position {
+    x: -8.662141
+    y: 9.414204
+  }
+}
+instances {
+  id: "target19"
+  prototype: "/example/target_round.go"
+  position {
+    x: 5.122415
+    y: -3.5531
+  }
+}
+instances {
+  id: "target20"
+  prototype: "/example/target_square.go"
+  position {
+    x: 6.697317
+    y: -2.589896
+  }
+}
+instances {
+  id: "coin1"
+  prototype: "/example/coin.go"
+  position {
+    x: -6.128283
+    y: 3.18032
+  }
+}
+instances {
+  id: "coin2"
+  prototype: "/example/coin.go"
+  position {
+    x: 7.385525
+    y: -2.997722
+  }
+}
+scale_along_z: 1
+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.1\\n"
+  "far_z: 1000.0\\n"
+  "auto_aspect_ratio: 1\\n"
+  "\"\n"
+  "}\n"
+  ""
+  position {
+    z: 5.0
+  }
+}
+embedded_instances {
+  id: "main"
+  data: "components {\n"
+  "  id: \"entity_picking\"\n"
+  "  component: \"/example/entity_picking.script\"\n"
+  "}\n"
+  ""
+}
+embedded_instances {
+  id: "targets"
+  children: "target1"
+  children: "target10"
+  children: "target11"
+  children: "target12"
+  children: "target13"
+  children: "target14"
+  children: "target15"
+  children: "target16"
+  children: "target17"
+  children: "target18"
+  children: "target19"
+  children: "target2"
+  children: "target20"
+  children: "target3"
+  children: "target4"
+  children: "target5"
+  children: "target6"
+  children: "target7"
+  children: "target8"
+  children: "target9"
+  data: ""
+}

+ 79 - 0
input/entity_picking/example/entity_picking.script

@@ -0,0 +1,79 @@
+local camera_math = require("example.camera_math")
+
+--- Performs a raycast from the camera through a screen position to find an entity.
+-- @param screen_x number The x-coordinate on the screen
+-- @param screen_y number The y-coordinate on the screen
+-- @param collision_groups table The collision groups to check against as array of hash values
+-- @return table|nil The first entity hit by the ray, or nil if nothing was hit
+local function pick_entity(screen_x, screen_y, collision_groups)
+	local from = camera_math.screen_to_world(screen_x, screen_y, 0, "/camera#camera")
+	local to = camera_math.screen_to_world(screen_x, screen_y, 100, "/camera#camera")
+	local results = physics.raycast(from, to, collision_groups, { all = false })
+	if not results then
+		return nil
+	end
+
+	return results[1]
+end
+
+function init(self)
+	-- Use the projection provided by the camera
+	msg.post("@render:", "use_camera_projection")
+
+	-- Acquire input focus to receive input events
+	msg.post(".", "acquire_input_focus")
+
+	self.input_pressed = false -- Tracks if the input is currently pressed
+	self.last_input = nil      -- Stores the last input action received
+	self.previous = nil        -- Keeps track of the previously highlighted entity
+end
+
+function update(self, dt)
+	if not self.last_input then
+		-- No input received yet
+		return
+	end
+
+	local result = pick_entity(self.last_input.screen_x, self.last_input.screen_y, { hash("target") })
+	if result then
+		-- Store in the result table the model URL of the entity just for convenience
+		result.model_url = msg.url(nil, result.id, "model")
+
+		-- Set the tint of the entity to highlight it
+		go.set(result.model_url, "tint.w", 1.5)
+
+		-- If the input is currently pressed, move the camera to the entity
+		if self.input_pressed then
+			-- We want to move the camera to only the X,Y of the entity, so we get its position
+			local move_to = go.get("/camera", "position")
+			move_to.x = result.position.x
+			move_to.y = result.position.y
+
+			go.cancel_animations("/camera", "position")
+			go.animate("/camera", "position", go.PLAYBACK_ONCE_FORWARD, move_to, go.EASING_INOUTQUAD, 0.5)
+		end
+
+		-- If the previously highlighted entity is different from the current entity, reset its tint
+		if self.previous and self.previous.id ~= result.id then
+			go.set(self.previous.model_url, "tint.w", 1)
+		end
+		self.previous = result
+	else
+		-- No entity was hit, so reset the tint of the previously highlighted entity
+		if self.previous then
+			go.set(self.previous.model_url, "tint.w", 1)
+			self.previous = nil
+		end
+	end
+end
+
+function on_input(self, action_id, action)
+	if action_id == hash("touch") then
+		-- "touch" is a screen touch or mouse click. We only want to react to the press event.
+		self.input_pressed = action.pressed
+	elseif not action_id then
+		-- If action_id is nil, it means that the action is a mouse move event.
+		-- "action" contains the mouse move event data. We want to store it for later use.
+		self.last_input = action
+	end
+end

+ 48 - 0
input/entity_picking/example/target_round.go

@@ -0,0 +1,48 @@
+embedded_components {
+  id: "model"
+  type: "model"
+  data: "mesh: \"/assets/models/kenney_prototype-kit/target-a-round.glb\"\n"
+  "name: \"{{NAME}}\"\n"
+  "materials {\n"
+  "  name: \"colormap\"\n"
+  "  material: \"/assets/materials/unlit.material\"\n"
+  "  textures {\n"
+  "    sampler: \"texture0\"\n"
+  "    texture: \"/assets/models/kenney_prototype-kit/Textures/colormap.png\"\n"
+  "  }\n"
+  "}\n"
+  ""
+  position {
+    y: -0.25
+  }
+  rotation {
+    y: 0.70710677
+    w: 0.70710677
+  }
+}
+embedded_components {
+  id: "collisionobject"
+  type: "collisionobject"
+  data: "type: COLLISION_OBJECT_TYPE_STATIC\n"
+  "mass: 0.0\n"
+  "friction: 0.1\n"
+  "restitution: 0.5\n"
+  "group: \"target\"\n"
+  "mask: \"default\"\n"
+  "embedded_collision_shape {\n"
+  "  shapes {\n"
+  "    shape_type: TYPE_SPHERE\n"
+  "    position {\n"
+  "    }\n"
+  "    rotation {\n"
+  "    }\n"
+  "    index: 0\n"
+  "    count: 1\n"
+  "  }\n"
+  "  data: 0.25\n"
+  "}\n"
+  "event_collision: false\n"
+  "event_contact: false\n"
+  "event_trigger: false\n"
+  ""
+}

+ 50 - 0
input/entity_picking/example/target_square.go

@@ -0,0 +1,50 @@
+embedded_components {
+  id: "model"
+  type: "model"
+  data: "mesh: \"/assets/models/kenney_prototype-kit/target-a-square.glb\"\n"
+  "name: \"{{NAME}}\"\n"
+  "materials {\n"
+  "  name: \"colormap\"\n"
+  "  material: \"/assets/materials/unlit.material\"\n"
+  "  textures {\n"
+  "    sampler: \"texture0\"\n"
+  "    texture: \"/assets/models/kenney_prototype-kit/Textures/colormap.png\"\n"
+  "  }\n"
+  "}\n"
+  ""
+  position {
+    y: -0.25
+  }
+  rotation {
+    y: 0.70710677
+    w: 0.70710677
+  }
+}
+embedded_components {
+  id: "collisionobject"
+  type: "collisionobject"
+  data: "type: COLLISION_OBJECT_TYPE_STATIC\n"
+  "mass: 0.0\n"
+  "friction: 0.1\n"
+  "restitution: 0.5\n"
+  "group: \"target\"\n"
+  "mask: \"default\"\n"
+  "embedded_collision_shape {\n"
+  "  shapes {\n"
+  "    shape_type: TYPE_BOX\n"
+  "    position {\n"
+  "    }\n"
+  "    rotation {\n"
+  "    }\n"
+  "    index: 0\n"
+  "    count: 3\n"
+  "  }\n"
+  "  data: 0.25\n"
+  "  data: 0.25\n"
+  "  data: 0.0625\n"
+  "}\n"
+  "event_collision: false\n"
+  "event_contact: false\n"
+  "event_trigger: false\n"
+  ""
+}

+ 55 - 0
input/entity_picking/game.project

@@ -0,0 +1,55 @@
+[project]
+title = input-entity_picking
+version = 1.0.0
+
+[bootstrap]
+main_collection = /example/entity_picking.collectionc
+
+[input]
+game_binding = /builtins/input/all.input_bindingc
+repeat_interval = 0.05
+
+[display]
+width = 720
+height = 720
+high_dpi = 1
+
+[script]
+shared_state = 1
+
+[label]
+subpixels = 1
+
+[sprite]
+subpixels = 1
+
+[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
+
+[graphics]
+texture_profiles = /all.texture_profiles
+
+[render]
+clear_color_blue = 0.9
+clear_color_green = 0.9
+clear_color_red = 0.9
+
+[physics]
+type = 3D
+max_fixed_timesteps = 1
+