diff --git a/README.md b/README.md index e3f6441..0038db4 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ How much time in seconds needed to change the current zoom to the desired zoom a ### collision_check -Checks a path from the camera to the operator anchor for the collisions and move it closer if necessary. +Checks a path from the camera to the operator anchor for the collisions and moves it closer if necessary. ### collision_distance @@ -406,19 +406,7 @@ local easing_linear = function(x) return x end operator.flight_look_easing = easing_linear ``` -### operator.flight_zoom_easing - -Easing function to update the camera zoom distance between motion points. - -```lua --- set a ready to use easing function -operator.flight_zoom_easing = operator.EASING_INOUT_QUAD --- or use your own -local easing_linear = function(x) return x end -operator.flight_zoom_easing = easing_linear -``` - -### Included Easing Functions +Included easing functions: - `operator.EASING_INOUT_SINE` - `operator.EASING_INOUT_CUBIC` diff --git a/main/car.script b/example/car.script similarity index 100% rename from main/car.script rename to example/car.script diff --git a/main/hint.gui b/example/hint.gui similarity index 100% rename from main/hint.gui rename to example/hint.gui diff --git a/main/main.collection b/example/main.collection similarity index 100% rename from main/main.collection rename to example/main.collection diff --git a/main/scenario.script b/example/scenario.script similarity index 100% rename from main/scenario.script rename to example/scenario.script diff --git a/game.project b/game.project index bfc2e36..1dd3f82 100644 --- a/game.project +++ b/game.project @@ -2,7 +2,8 @@ include_dirs = operator [project] -title = Defold Operator +title = Operator +version = 1.1 dependencies#0 = https://github.com/indiesoftby/defold-pointer-lock/archive/1b5e05b94491bb7e041052990122dd846e1605a9.zip [bootstrap] diff --git a/operator/bezier.lua b/operator/bezier.lua index d6de907..642c443 100644 --- a/operator/bezier.lua +++ b/operator/bezier.lua @@ -18,37 +18,53 @@ local function int_lerp(t, a, b) return a + (b - a) * t end +local function linear_bezier(time, lerp, p1, p2) + local result = p1 == p2 and p1 or lerp(time, p1, p2) + return result +end + local function quad_bezier(time, lerp, p1, p2, p3) - local a = lerp(time, p1, p2) - local b = lerp(time, p2, p3) - local result = lerp(time, a, b) + local a = linear_bezier(time, lerp, p1, p2) + local b = linear_bezier(time, lerp, p2, p3) + local result = linear_bezier(time, lerp, a, b) return result end local function cubic_bezier(time, lerp, p1, p2, p3, p4) local a = quad_bezier(time, lerp, p1, p2, p3) local b = quad_bezier(time, lerp, p2, p3, p4) - local result = lerp(time, a, b) + local result = linear_bezier(time, lerp, a, b) return result end +local bezier_funcs = { + [2] = linear_bezier, + [3] = quad_bezier, + [4] = cubic_bezier +} + -- Public function bezier.new(points, samples_count, length_func, lerp_func) - assert(#points >= 3 and #points <= 4, 'Only 3 or 4 bezier points supported.') - assert(points[1] ~= points[#points], 'No distance between the start and end points.') + assert(#points >= 2 and #points <= 4, 'Only 2-4 bezier points are supported.') local self = setmetatable({ }, { __index = bezier }) local is_integer = type(points[1]) == 'integer' self.length_func = length_func or (is_integer and int_length or vmath.length) self.lerp_func = lerp_func or (is_integer and int_lerp or vmath.lerp) - self.bezier_func = #points == 4 and cubic_bezier or quad_bezier - - self.points = points + self.bezier_func = bezier_funcs[#points] self.samples = { } - local samples_count = (samples_count and samples_count >= 1) and samples_count or 1 + self.origin = points[1] + self.points = { } + for index, point in ipairs(points) do + self.points[index] = point - self.origin + end + + local samples_count = samples_count or 1 + samples_count = (#points > 2 and samples_count > 1) and samples_count or 1 + local previous_position = points[1] local passed_length = 0 @@ -66,7 +82,7 @@ function bezier.new(points, samples_count, length_func, lerp_func) end function bezier.position(self, time) - return self.bezier_func(time, self.lerp_func, unpack(self.points)) + return self.origin + self.bezier_func(time, self.lerp_func, unpack(self.points)) end function bezier.uniform_position(self, time) diff --git a/operator/operator.lua b/operator/operator.lua index acb6e77..91ae7b5 100644 --- a/operator/operator.lua +++ b/operator/operator.lua @@ -90,7 +90,6 @@ end operator.camera_collisions_groups = { hash 'default' } operator.flight_bezier_samples_count = 32 operator.flight_look_easing = operator.EASING_INOUT_QUAD -operator.flight_zoom_easing = operator.EASING_INOUT_QUAD -- Private Properties diff --git a/operator/operator/operator.script b/operator/operator/operator.script index abfbecb..5c65c30 100644 --- a/operator/operator/operator.script +++ b/operator/operator/operator.script @@ -42,6 +42,7 @@ go.property('collision_distance', 0.3) go.property('ground_align_factor', 0.5) go.property('ground_align_smoothness', 0.5) +-- -- Helpers local debug_colors = { @@ -56,6 +57,27 @@ local function quat_from_look(look) return quat_z * quat_y * quat_x end +local function short_rotation_angle(original, target) + local planned_rotation = target - original + local nearest_angle = target + + if planned_rotation > 180 then + nearest_angle = target - 360 + elseif planned_rotation < -180 then + nearest_angle = target + 360 + end + + return nearest_angle +end + +local function short_rotation_look(original, target) + local nearest_look = vmath.vector3(target) + nearest_look.x = short_rotation_angle(original.x, target.x) + nearest_look.y = short_rotation_angle(original.y, target.y) + nearest_look.z = short_rotation_angle(original.z, target.z) + return nearest_look +end + local function clamp_angle(angle, min, max) local angle = angle @@ -69,6 +91,50 @@ local function clamp_angle(angle, min, max) return angle end +local function camera_distance(look, zoom) + local look_quat = quat_from_look(look) + local camera_distance = vmath.rotate(look_quat, vmath.vector3(0, 0, zoom)) + return camera_distance +end + +local function move_motion_point_with_object(point, object_movement) + point.position = point.position + object_movement + point.camera_position = point.camera_position + object_movement + + if point.bezier_path then + point.bezier_path.origin = point.bezier_path.origin + object_movement + end +end + +-- +-- Debug + +local function debug_bezier_lines(self, target) + for index = 1, #target.bezier_path.samples do + local start_progress = (index - 1) / #target.bezier_path.samples + local start_point = target.bezier_path:uniform_position(start_progress) + + local end_pregress = index / #target.bezier_path.samples + local end_point = target.bezier_path:uniform_position(end_pregress) + + local line = { + start_point = start_point, + end_point = end_point, + color = debug_colors.red + } + table.insert(self.debug_lines, line) + end +end + +local function draw_debug_lines(self) + for _, line in ipairs(self.debug_lines) do + msg.post('@render:', hash 'draw_line', line) + end + + self.debug_lines = { } +end + +-- -- Activation local function activate(self, is_active) @@ -92,6 +158,7 @@ local function activate(self, is_active) end end +-- -- Input local function handle_input(self, input) @@ -125,8 +192,20 @@ local function handle_input(self, input) end end +-- -- Attachment and Detachment +local function attach_to_object(self, object) + if self.object == object then + return + end + + self.object = object + self.object_position = go.get_world_position(object) + + msg.post(object, hash 'operator_attached', { operator = self.urls.this }) +end + local function detach_from_object(self) if not self.object then return @@ -134,67 +213,49 @@ local function detach_from_object(self) local object = self.object - -- Detach from the object - - go.set_parent('.', nil, false) self.object = nil + self.object_position = nil + self.ground_align = 0 self.ground_align_target = 0 - -- Restore global position - - local object_position = go.get_world_position(object) - local object_rotation = go.get_world_rotation(object) - - local local_position = vmath.rotate(object_rotation, go.get_position('.')) - local global_position = local_position + object_position - - go.set_position(global_position, '.') - - -- Restore global rotation - - local object_look = go.get(object, hash 'euler') - - self.look = self.look + object_look - self.look_target = self.look - - go.set('.', hash 'euler', self.look) - msg.post(object, hash 'operator_detached', { operator = self.urls.this }) end -local function attach_to_object(self, object) - if self.object == object then +-- +-- Zoom collapsing + +local function collapse_zoom(self) + if self.zoom == 0 then return end + + self.position = self.position + camera_distance(self.look, self.zoom) + go.set_position(self.position, '.') - -- Attach to the object + self.camera_position = vmath.vector3() + go.set_position(self.camera_position, self.urls.camera) - go.set_parent('.', object, false) - self.object = object - - -- Restore local position - - local object_position = go.get_world_position(object) - local object_rotation = go.get_world_rotation(object) + self.zoom = 0 + self.zoom_target = 0 +end - local global_position = go.get_world_position('.') - local local_position = vmath.rotate(vmath.conj(object_rotation), global_position - object_position) - - go.set_position(local_position, '.') - - -- Restore local rotation +local function uncollapse_zoom(self, zoom) + if zoom == 0 then + return + end - local object_look = go.get(object, hash 'euler') - - self.look = self.look - object_look - self.look_target = self.look + self.position = self.position - camera_distance(self.look, zoom) + go.set_position(self.position, '.') - go.set('.', hash 'euler', self.look) + self.camera_position = vmath.vector3(0, 0, zoom) + go.set_position(self.camera_position, self.urls.camera) - msg.post(object, hash 'operator_attached', { operator = self.urls.this }) + self.zoom = zoom + self.zoom_target = zoom end +-- -- Motion local function stop_motion(self) @@ -209,6 +270,7 @@ local function stop_motion(self) self.motion_sequence = nil self.motion_timer = nil self.motion_observer = nil + self.motion_speed = 0 end local function start_motion(self, motion_sequence) @@ -217,79 +279,55 @@ local function start_motion(self, motion_sequence) local original = motion_sequence[1] local target = motion_sequence[2] - + -- Sync attached and detached states - + if original.object ~= target.object then if original.object ~= nil then - local object_rotation = go.get_world_rotation(original.object) - detach_from_object(self) - - if original.path_anchor then - original.path_anchor = vmath.rotate(object_rotation, original.path_anchor) - end end if target.object ~= nil then - local object_rotation = go.get_world_rotation(target.object) - attach_to_object(self, target.object) - - if original.path_anchor then - original.path_anchor = vmath.rotate(vmath.conj(object_rotation), original.path_anchor) - end end - - original.position = go.get_position('.') - original.look = self.look end + if target.object ~= nil then + move_motion_point_with_object(target, self.object_position) + end + -- Make a bezier path - local path_points = { original.position } + local path_points = { original.camera_position } local after = motion_sequence[3] if after and target.bezier then - local path_anchor_direction = vmath.normalize(after.position - original.position) + local path_anchor_direction = vmath.normalize(after.camera_position - original.camera_position) target.path_anchor = path_anchor_direction end - local segment_distance = vmath.length(target.position - original.position) + local segment_distance = vmath.length(target.camera_position - original.camera_position) - if original.path_anchor then -- Add the first anchor - local original_anchor = original.position + original.path_anchor * segment_distance / 2 + local original_anchor = original.camera_position + original.path_anchor * segment_distance / 2 table.insert(path_points, original_anchor) end if target.path_anchor then -- Add the second anchor - local target_anchor = target.position - target.path_anchor * segment_distance / 2 + local target_anchor = target.camera_position - target.path_anchor * segment_distance / 2 table.insert(path_points, target_anchor) end - table.insert(path_points, target.position) + table.insert(path_points, target.camera_position) - if #path_points > 2 then - target.bezier_path = bezier.new(path_points, operator.flight_bezier_samples_count) - end + target.bezier_path = bezier.new(path_points, operator.flight_bezier_samples_count) -- Duration and distance - local original_camera_quat = quat_from_look(original.look) - local original_camera_distance = vmath.rotate(original_camera_quat, vmath.vector3(0, 0, original.zoom)) - local original_camera_position = original.position + original_camera_distance - original.camera_position = original_camera_position - - local target_camera_quat = quat_from_look(target.look) - local target_camera_distance = vmath.rotate(target_camera_quat, vmath.vector3(0, 0, target.zoom)) - local target_camera_position = target.position + target_camera_distance - target.camera_position = target_camera_position - - local distance = vmath.length(original_camera_position - target_camera_position) + local distance = vmath.length(original.camera_position - target.camera_position) target.distance = distance local original_speed = original.inout and 0 or original.speed @@ -304,10 +342,15 @@ local function follow_sequence(self, sequence, sender) if self.motion_sequence then stop_motion(self) end + + -- Starting point + + collapse_zoom(self) local previous_point = { object = self.object, - position = go.get_position('.'), + position = self.position, + camera_position = self.position, look = self.look, zoom = self.zoom, speed = self.motion_speed @@ -317,14 +360,14 @@ local function follow_sequence(self, sequence, sender) -- Prepare the sequence - for _, item in ipairs(sequence or { }) do + for _, item in ipairs(sequence) do local point if type(item) == 'userdata' then -- Item is checkpoint url local checkpoint = msg.url(item) - local position = go.get_position(checkpoint) + local position = go.get_world_position(checkpoint) local object = go.get_parent(checkpoint) local look = go.get(checkpoint, hash 'euler') @@ -342,36 +385,26 @@ local function follow_sequence(self, sequence, sender) } else -- Item is checkpoint table - point = item - point.object = point.object - point.position = point.position or vmath.vector3() - point.look = point.look or previous_point.look - point.zoom = point.zoom or previous_point.zoom - point.speed = point.speed or previous_point.speed - point.inout = point.inout or false - point.bezier = not point.bezier == false - end - - local function correct_look(from, to) - local planned_turning = to - from - - if planned_turning > 180 then - return to - 360 - elseif planned_turning < -180 then - return to + 360 - else - return to - end + point = { + object = item.object, + position = item.position or vmath.vector3(), + look = item.look or previous_point.look, + zoom = item.zoom or previous_point.zoom, + speed = item.speed or previous_point.speed, + inout = item.inout or false, + bezier = not item.bezier == false + } end - point.look.x = correct_look(previous_point.look.x, point.look.x) - point.look.y = correct_look(previous_point.look.y, point.look.y) - point.look.z = correct_look(previous_point.look.z, point.look.z) - + point.look = short_rotation_look(previous_point.look, point.look) + point.camera_position = point.position + camera_distance(point.look, point.zoom) + table.insert(motion_sequence, point) previous_point = point end + -- Start + self.motion_observer = sender start_motion(self, motion_sequence) end @@ -402,6 +435,7 @@ local function unfollow(self) detach_from_object(self) end +-- -- Ground normal local function did_update_ground_normal(self, ground_normal) @@ -424,71 +458,70 @@ local function did_update_ground_normal(self, ground_normal) self.ground_align_target = operator.angle_between(vector_up, ground_normal) * camera_direction_factor * self.ground_align_factor end -local function debug_bezier_lines(self, target) - for index = 1, #target.bezier_path.samples do - local start_progress = (index - 1) / #target.bezier_path.samples - local start_point = target.bezier_path:uniform_position(start_progress) +-- +-- Updating - local end_pregress = index / #target.bezier_path.samples - local end_point = target.bezier_path:uniform_position(end_pregress) +local function update_position(self) + local object_position = self.object_position + self.object_position = go.get_world_position(self.object) + local object_movement = self.object_position - object_position - if target.object then - local object_position = go.get_world_position(target.object) - local object_rotation = go.get_world_rotation(target.object) - - start_point = vmath.rotate(object_rotation, start_point) - start_point = start_point + object_position + if vmath.length(object_movement) == 0 then + return + end - end_point = vmath.rotate(object_rotation, end_point) - end_point = end_point + object_position - end + local position = self.position + object_movement - local line = { - start_point = start_point, - end_point = end_point, - color = debug_colors.red - } - table.insert(self.debug_lines, line) + if self.position ~= position then + self.position = position + go.set_position(self.position, '.') end -end --- Updating + for _, motion_point in ipairs(self.motion_sequence or { }) do + move_motion_point_with_object(motion_point, object_movement) + end +end local function update_motion(self, dt) local original = self.motion_sequence[1] local target = self.motion_sequence[2] - -- Timer + -- Update timer self.motion_timer = self.motion_timer + dt local time_progress = self.motion_timer / target.duration - if time_progress >= 1 then - if #self.motion_sequence > 2 then - -- There is a next point, glide to it + local is_the_end = time_progress >= 1 and #self.motion_sequence == 2 + local is_gliding = time_progress >= 1 and #self.motion_sequence > 2 + + if is_the_end then + -- This was the last point, + -- so just align the position to the finish point + time_progress = 1 + end + + if is_gliding then + -- There is a next point, + -- so skip this motion update and + -- glide to the next motion segment local overtime = self.motion_timer - target.duration table.remove(self.motion_sequence, 1) self.motion_timer = nil - if not target.inout then - msg.post(self.motion_observer, hash 'motion_point', { - object = target.object, - checkpoint = target.checkpoint - }) - end + local message = { + object = target.object, + checkpoint = target.checkpoint + } + msg.post(self.motion_observer, hash 'motion_point', message) start_motion(self) update_motion(self, overtime) return - else - -- This is the end, align to the finish point - time_progress = 1 - end end - -- Speed and progress + -- Update speed and progress local path_progress @@ -514,42 +547,31 @@ local function update_motion(self, dt) path_progress = 1 end - -- Position + -- Update position - local position + local camera_position = target.bezier_path:uniform_position(path_progress) - if target.bezier_path then - position = target.bezier_path:uniform_position(path_progress) - - if self.is_debug then - debug_bezier_lines(self, target) - end - else - position = original.position + (target.position - original.position) * path_progress - end + self.position = camera_position + go.set_position(self.position, '.') - go.set_position(position, '.') - - -- Look + -- Update look local look_easing = operator.flight_look_easing(path_progress) self.look = operator.lerp(look_easing, original.look, target.look) self.look_target = self.look go.set('.', hash 'euler', self.look) - -- Zoom - - local zoom_easing = operator.flight_zoom_easing(path_progress) - local zoom = operator.lerp(zoom_easing, original.zoom, target.zoom) - self.zoom = zoom - self.zoom_target = zoom + -- Debug - self.camera_position = vmath.vector3(0, 0, zoom) - go.set_position(self.camera_position, self.urls.camera) + if self.is_debug then + debug_bezier_lines(self, target) + end - -- Check for finishing + -- Finish - if time_progress == 1 and #self.motion_sequence == 2 then + if is_the_end then + uncollapse_zoom(self, target.zoom) + local observer = self.motion_observer stop_motion(self) @@ -659,15 +681,15 @@ local function update_look(self, dt) camera_position = vmath.vector3(0, 0, zoom) if self.collision_check and zoom > 0 then - local ray_from = go.get_world_position('.') - local camera_sensor = vmath.vector3(0, 0, zoom_target + self.collision_distance) + local ray_from = self.position + local camera_sensor = vmath.vector3(0, 0, zoom + self.collision_distance) local world_rotation = go.get_world_rotation('.') local ray_to = ray_from + vmath.rotate(world_rotation, camera_sensor) local result = physics.raycast(ray_from, ray_to, operator.camera_collisions_groups) if result then - zoom = (zoom_target + self.collision_distance) * result.fraction - self.collision_distance + zoom = (zoom + self.collision_distance) * result.fraction - self.collision_distance end end @@ -680,7 +702,7 @@ local function update_look(self, dt) if self.is_debug and zoom ~= 0 and not self.is_active then local line = { - start_point = go.get_world_position('.'), + start_point = self.position, end_point = go.get_world_position(self.urls.camera), color = debug_colors.blue } @@ -691,14 +713,6 @@ local function update_look(self, dt) self.zoom_target = zoom_target end -local function draw_debug_lines(self) - for _, line in ipairs(self.debug_lines) do - msg.post('@render:', hash 'draw_line', line) - end - - self.debug_lines = { } -end - -- Lifecycle function init(self) @@ -743,6 +757,7 @@ function init(self) go.set(camera_component, hash 'far_z', self.camera_far_z) self.object = nil + self.position = go.get_world_position('.') self.is_debug = false self.debug_lines = { } @@ -756,10 +771,13 @@ function init(self) end function update(self, dt) + if self.object then + update_position(self) + end + if self.motion_timer then update_motion(self, dt) else - self.motion_speed = 0 update_look(self, dt) end