AABB

This example demonstrates how to use the model.get_aabb() function in a 3D scene.

Project files

This example shows how to work with Axis-Aligned Bounding Boxes (AABB) in a 3D scene. The setup consists of falling cubes that are dynamically tracked by a camera using their combined bounding box. The example demonstrates:

Press SPACE or click to spawn new cubes. The camera will automatically adjust to keep all objects in view based on their combined bounding box.

The models used in this example are from Kenney’s Prototype Kit, licensed under CC0.

Scripts

aabb.script

--
-- Dynamic bounding box - it tracks the bounding box of the objects in the scene
--

--- Create a new instance
-- @return table - the bounding box instance
local function bbox_new()
	return {
		objects = {}, -- dict for iteration
		count = 0,
		min = vmath.vector3(),
		max = vmath.vector3()
	}
end

--- Add an object to the bounding box
-- @param bbox table - the bounding box instance
-- @param obj_id hash - the object id
-- @param aabb table - the aabb of the object
local function bbox_add(bbox, obj_id, aabb)
	if not aabb then
		aabb = model.get_aabb(msg.url(nil, obj_id, "model"))
	else
		assert(types.is_vector3(aabb.min) and types.is_vector3(aabb.max), "AABB is not valid")
	end

	local entry = {
		id = obj_id,
		position = go.get_position(obj_id),
		aabb = aabb
	}
	bbox.objects[obj_id] = entry
	bbox.count = bbox.count + 1
end

--- Remove an object from the bounding box
-- @param bbox table - the bounding box instance
-- @param obj_id hash - the object id
local function bbox_remove(bbox, obj_id)
	bbox.objects[obj_id] = nil
	bbox.count = bbox.count - 1
end

--- Update the bounding box
-- @param bbox table - the bounding box instance
local function bbox_update_all(bbox)
	bbox.min = vmath.vector3()
	bbox.max = vmath.vector3()
	for _, entry in pairs(bbox.objects) do
		local pos = go.get_position(entry.id)
		entry.position = pos

		bbox.min.x = math.min(bbox.min.x, entry.aabb.min.x + pos.x)
		bbox.min.y = math.min(bbox.min.y, entry.aabb.min.y + pos.y)
		bbox.min.z = math.min(bbox.min.z, entry.aabb.min.z + pos.z)
		bbox.max.x = math.max(bbox.max.x, entry.aabb.max.x + pos.x)
		bbox.max.y = math.max(bbox.max.y, entry.aabb.max.y + pos.y)
		bbox.max.z = math.max(bbox.max.z, entry.aabb.max.z + pos.z)
	end
end

--- Compute the bounding box
-- @param bbox table - the bounding box instance
-- @return table - result with {center, min, max, radius}
local function bbox_compute(bbox)
	local center = (bbox.min + bbox.max) * 0.5
	local radius = vmath.length(bbox.max - bbox.min) * 0.5
	return {
		center = center,
		min = bbox.min,
		max = bbox.max,
		radius = radius
	}
end

--
-- Helper functions
--

--- Add a cube to the scene
-- @param self table - the script instance
-- @param x number - the x coordinate
-- @param y number - the y coordinate
-- @param z number - the z coordinate
-- @param color string - the color of the cube - "red" or "white"
local function add_cube(self, x, y, z, color)
	if self.bbox.count >= sys.get_config_int("model.max_count") then
		print("Increase `model.max_count` and `physics.max_collision_object_count` values!")
		return
	end

	local url = color == "red" and "#factory_box2" or "#factory_box1"
	local obj_id = factory.create(url, vmath.vector3(x, y, z))
	bbox_add(self.bbox, obj_id)

	go.animate(msg.url(nil, obj_id, "model"), "tint.w", go.PLAYBACK_ONCE_BACKWARD, 3, go.EASING_INQUAD, 0.5)
end

--
-- Main script
--

function init(self)
	-- Acquire input focus to receive input events
	msg.post(".", "acquire_input_focus")

	-- Get the camera default rotation
	self.camera_euler = go.get("/camera", "euler")

	-- Create a new dynamic bounding box instance
	self.bbox = bbox_new()

	-- Add some cubes to the scene at (0, 1-5, 0) coordinates
	for i = 1, 10 do
		local cube_color = i % 2 == 0 and "red" or "white"
		add_cube(self, (math.random() - 0.5) * 0.1, i / 2, (math.random() - 0.5) * 0.1, cube_color)
	end
	bbox_update_all(self.bbox)

	-- Compute the initial bounding box data
	self.view = bbox_compute(self.bbox)
end

function update(self, dt)
	bbox_update_all(self.bbox)

	-- Current bounding box data
	local current = bbox_compute(self.bbox)

	-- Animate the values for smooth camera movement
	local t = 0.05
	self.view.center = vmath.lerp(t, self.view.center, current.center)
	self.view.radius = vmath.lerp(t, self.view.radius, current.radius)

	-- Calculate camera position and rotation
	local camera_yaw = vmath.quat_rotation_y(math.rad(self.camera_euler.y))
	local camera_pitch = vmath.quat_rotation_x(math.rad(self.camera_euler.x))
	local camera_rotation = camera_yaw * camera_pitch
	local camera_zoom = 1.05 * self.view.radius / math.tan(0.5 * go.get("/camera#camera", "fov"))
	local camera_position = self.view.center + vmath.rotate(camera_rotation, vmath.vector3(0, 0, camera_zoom))
	go.set("/camera", "position", camera_position)
	go.set("/camera", "rotation", camera_rotation)

	-- Uncomment to benchmark
	-- add_cube(self, math.random(-3, 3), 10, math.random(-3, 3))
	-- add_cube(self, math.random(-3, 3), 10, math.random(-3, 3), "red")
end

function on_input(self, action_id, action)
	-- Add a cube to the scene when the mouse button / space key is pressed
	if (action_id == hash("touch") or action_id == hash("key_space")) and action.pressed then
		local colors = {"red", "white"}
		add_cube(self, (math.random() - 0.5) * 0.5, 10, (math.random() - 0.5) * 0.5, colors[math.random(1, 2)])
	end
end