From 3ed80d0c9507b034715885a5fa32de00d266900c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 9 Sep 2026 19:26:17 +0300 Subject: [PATCH 1/6] feat: custom curves --- res/modules/animation.lua | 73 ++++++++++++++++++++-------- res/modules/internal/formats/vca.lua | 14 ++++-- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index a54a090b1..5b768a70a 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -9,6 +9,7 @@ local this = { INT_CONST = 1, INT_LINEAR = 2, INT_BEZIER = 3, + INT_CUSTOM = 4, TRACE_CODEGEN = false, } @@ -53,10 +54,7 @@ local function bezier_interpolation(k0, k1, t) return bezier(k0.value, k0.ry, k1.ly, k1.value, u) end -local patterns = { - {name="sint", pattern="sin(t)"}, - {name="sint2", pattern="sin(t * 2)"}, -} +local patterns = {} local exclude_patters = { "end", (string.pattern_safe("'")), @@ -127,9 +125,24 @@ local env = { local t = (frame - left.frame) / (right.frame - left.frame) if interp == INT_BEZIER then return bezier_interpolation(left, right, t) + elseif type(interp) == "function" then + return interp(left.value, right.value, t) end return left.value * (1.0 - t) + right.value * t end, + value_at_custom = function(keys, frame, func) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + return func(left, right, t) + end, set_matrix = function(target, matrix) local info = mat4.decompose(matrix) if info then @@ -176,9 +189,18 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) keysets[lineset.target_name] = target_keysets end target_keysets[i] = line.keys - code = code .. string.format( - "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", - i, lineset.target_name, i, raw_track.fps, line.interp) + + if line.curve_func then + local valueat = string.format("curves[%s]", string.escape(line.curve_func)) + + code = code .. string.format( + "\n local l%d = value_at_custom(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, valueat) + else + code = code .. string.format( + "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, line.interp) + end end if line.channel == this.CH_TRANSLATE then @@ -229,7 +251,7 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) return code end -local function codegen_rig_target(raw_track, memoised, keysets) +local function codegen_rig_target(raw_track, context) local code = "\n if target.set_matrix and target.index then\n" code = code .. " local dst = DST\n" for bone, lineset in pairs(raw_track.linesets) do @@ -237,7 +259,7 @@ local function codegen_rig_target(raw_track, memoised, keysets) goto continue end local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, true) + raw_track, lineset, context.memoised, context.keysets, true) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" @@ -246,7 +268,7 @@ local function codegen_rig_target(raw_track, memoised, keysets) return code .. " end" end -local function codegen_object_target(raw_track, memoised, keysets) +local function codegen_object_target(raw_track, context) local code = "\n if target.set_pos then\n" code = code .. " local dst = DST\n" local lineset = raw_track.linesets[""] @@ -254,13 +276,13 @@ local function codegen_object_target(raw_track, memoised, keysets) return "" end local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, true) + raw_track, lineset, context.memoised, context.keysets, true) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " set_matrix(target, dst)\n" return code .. " end" end -local function codegen_camera_target(raw_track, memoised, keysets) +local function codegen_camera_target(raw_track, context) local code = "\n if target.set_zoom then\n" code = code .. " local zoom = 1.0\n" local lineset = raw_track.linesets[""] @@ -268,7 +290,7 @@ local function codegen_camera_target(raw_track, memoised, keysets) return "" end local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, false) + raw_track, lineset, context.memoised, context.keysets, false) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " target:set_zoom(zoom)\n" return code .. " end" @@ -276,15 +298,24 @@ end function this.compile_track(raw_track, track_name) local code = "" - local memoised = {} - local keysets = {} + local context = { + memoised = {}, + keysets = {}, + curves = {}, + } + for name, curve in pairs(raw_track.curves) do + context.curves[name] = load(string.format( + "return function(kl, kr, t) return %s end", + process_expression(curve.func, context.memoised) + ), "", "t", env)() + end - code = code .. codegen_rig_target(raw_track, memoised, keysets) - code = code .. codegen_object_target(raw_track, memoised, keysets) - code = code .. codegen_camera_target(raw_track, memoised, keysets) + code = code .. codegen_rig_target(raw_track, context) + code = code .. codegen_object_target(raw_track, context) + code = code .. codegen_camera_target(raw_track, context) local memoised_code = "" - for name, expression in pairs(memoised) do + for name, expression in pairs(context.memoised) do memoised_code = memoised_code .. "\n local " .. name .. " = " .. expression end @@ -302,7 +333,9 @@ function this.compile_track(raw_track, track_name) end local generator, err = load( - src, "", "bt", table.extend({keysets = keysets}, env)) + src, "", "bt", table.extend({ + keysets = context.keysets, curves = context.curves + }, env)) if not generator then error(err) end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 0faa89404..8146ffff6 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -1,7 +1,6 @@ local internals = __vc_internals local DEFAULT_FPS = 60 -local INT_BEZIER = animation.INT_BEZIER local action_to_channel = { move = animation.CH_TRANSLATE, @@ -29,14 +28,19 @@ local function parse_configure(raw_track, node) end local function parse_curve(line, node) - line.interp = curve_to_interp[node.curve] + if node.curve:starts_with(".") then + line.interp = animation.INT_CUSTOM + line.curve_func = node.curve:sub(2) + else + line.interp = curve_to_interp[node.curve] + end line.keys = {} for j, key_node in ipairs(node) do local keyframe = { frame = tonumber(key_node.frame), value = tonumber(key_node.value), } - if line.interp == INT_BEZIER then + if line.interp == animation.INT_BEZIER then keyframe.lx = tonumber(key_node.lx) keyframe.ly = tonumber(key_node.ly) keyframe.rx = tonumber(key_node.rx) @@ -51,6 +55,7 @@ local function parse_track(root) duration = math.huge, fps = DEFAULT_FPS, linesets = {}, + curves = {}, } local linesets = raw_track.linesets for i, node in ipairs(root) do @@ -61,6 +66,9 @@ local function parse_track(root) if tag == "configure" then parse_configure(raw_track, node) goto continue + elseif tag == "curve" then + raw_track.curves[node.name] = node + goto continue end local target_type = nil From 48ca3ab5f9dc8f3e3ab7dcd08c3c8b7360fdcf36 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 00:11:04 +0300 Subject: [PATCH 2/6] move animation code generation to separate module --- res/modules/animation.lua | 316 +-------------------- res/modules/internal/animation_codegen.lua | 298 +++++++++++++++++++ res/modules/internal/formats/vca.lua | 2 +- res/scripts/stdlib.lua | 1 + 4 files changed, 313 insertions(+), 304 deletions(-) create mode 100644 res/modules/internal/animation_codegen.lua diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 5b768a70a..c42b15d25 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -1,22 +1,5 @@ local internals = __vc_internals -local this = { - CH_TRANSLATE = 1, - CH_ROTATE = 2, - CH_SCALE = 3, - CH_ZOOM = 4, - - INT_CONST = 1, - INT_LINEAR = 2, - INT_BEZIER = 3, - INT_CUSTOM = 4, - - TRACE_CODEGEN = false, -} - -local INT_CONST = this.INT_CONST -local INT_BEZIER = this.INT_BEZIER - local function bezier(a, b, c, d, u) local s = 1 - u return s * s * s * a + @@ -54,296 +37,23 @@ local function bezier_interpolation(k0, k1, t) return bezier(k0.value, k0.ry, k1.ly, k1.value, u) end -local patterns = {} -local exclude_patters = { - "end", - (string.pattern_safe("'")), - (string.pattern_safe('"')), - (string.pattern_safe("--")), - (string.pattern_safe("..")), -} +local this = { + CH_TRANSLATE = 1, + CH_ROTATE = 2, + CH_SCALE = 3, + CH_ZOOM = 4, --- TODO: replace with actual expression -> lua translator -local function process_expression(src, memoised) - for i, pattern in ipairs(exclude_patters) do - if src:find(pattern) then - debug.print(exclude_patters) - error("invalid syntax "..string.escape(src)) - end - end - for i, pattern in ipairs(patterns) do - local pattern_safe = string.pattern_safe(pattern.pattern) - if src:find(pattern_safe) then - memoised[pattern.name] = pattern.pattern - src = src:gsub(pattern_safe, pattern.name) - end - end - return src -end + INT_CONST = 1, + INT_LINEAR = 2, + INT_BEZIER = 3, + INT_CUSTOM = 4, -local function key_neighbors(keys, frame) - local left = 1 - local right = #keys + TRACE_CODEGEN = false, - while left <= right do - local mid = math.floor((left + right) / 2) - - if keys[mid].frame < frame then - left = mid + 1 - elseif keys[mid].frame > frame then - right = mid - 1 - else - return mid, mid - end - end - if left > #keys then - left = #keys - end - return right, left -end - -local env = { - mat4 = mat4, - e = math.exp(1), - X = {1, 0, 0}, - Y = {0, 1, 0}, - Z = {0, 0, 1}, - DST = mat4.idt(), - value_at = function(keys, frame, interp) - local left, right = key_neighbors(keys, frame) - if left == right then - return keys[left].value - end - left = keys[left] - if interp == INT_CONST then - return left.value - end - right = keys[right] - if left == nil then - return right.value - end - local t = (frame - left.frame) / (right.frame - left.frame) - if interp == INT_BEZIER then - return bezier_interpolation(left, right, t) - elseif type(interp) == "function" then - return interp(left.value, right.value, t) - end - return left.value * (1.0 - t) + right.value * t - end, - value_at_custom = function(keys, frame, func) - local left, right = key_neighbors(keys, frame) - if left == right then - return keys[left].value - end - left = keys[left] - right = keys[right] - if left == nil then - return right.value - end - local t = (frame - left.frame) / (right.frame - left.frame) - return func(left, right, t) - end, - set_matrix = function(target, matrix) - local info = mat4.decompose(matrix) - if info then - local pos = info.translation - local rot = info.rotation - local scale = info.scale - if target.set_pos then - target:set_pos(pos) - end - if target.set_rot then - target:set_rot(rot) - end - if target.set_scale then - target:set_scale(scale) - end - end - end, - dump = debug.print -} - -local math_funcs = { - "sqrt", "min", "max", "deg", "rad", "log", "log10", "floor", "ceil", "sin", - "tan", "noise", "noise2", "sign", "round", "exp", "pi", "e" -} -for _, name in ipairs(math_funcs) do - env[name] = math[name] -end - -local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) - local lines = lineset.lines - local code = "" - local has_tsf = false - local translation = {false, false, false} - local rotation = {false, false, false} - local scale = {false, false, false} - for i, line in ipairs(lines) do - if line.expression then - code = code .. "\n local l" .. i .. " = (" .. - process_expression(line.expression, memoised) .. ")" - elseif line.keys then - local target_keysets = keysets[lineset.target_name] - if not target_keysets then - target_keysets = {} - keysets[lineset.target_name] = target_keysets - end - target_keysets[i] = line.keys - - if line.curve_func then - local valueat = string.format("curves[%s]", string.escape(line.curve_func)) - - code = code .. string.format( - "\n local l%d = value_at_custom(keysets['%s'][%d], t * %s, %s)", - i, lineset.target_name, i, raw_track.fps, valueat) - else - code = code .. string.format( - "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", - i, lineset.target_name, i, raw_track.fps, line.interp) - end - end - - if line.channel == this.CH_TRANSLATE then - translation[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_ROTATE then - rotation[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_SCALE then - scale[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_ZOOM then - code = code .. "\n zoom = l" .. i - end - end - - if not has_tsf or not use_tsf then - return code - end - - code = code .. "\n mat4.idt(dst)" - if translation[1] or translation[2] or translation[3] then - code = code .. "\n mat4.translate(dst, {" .. - (translation[1] and ("l" .. translation[1]) or '0').. ", " .. - (translation[2] and ("l" .. translation[2]) or '0').. ", " .. - (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" - end - - local axis_names = {"X", "Y", "Z"} - local axis_indices = {X=1, Y=2, Z=3} - local rotation_order = raw_track.rotation_order - for i=1,3 do - local axis = axis_indices[rotation_order[i]] - local var = rotation[axis] - if var then - code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. - ", l" .. var .. ", dst)" - end - end - - if scale[1] or scale[2] or scale[3] then - code = code .. "\n mat4.scale(dst, {" .. - (scale[1] and ("l" .. scale[1]) or '1').. ", " .. - (scale[2] and ("l" .. scale[2]) or '1').. ", " .. - (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" - end - - return code -end - -local function codegen_rig_target(raw_track, context) - local code = "\n if target.set_matrix and target.index then\n" - code = code .. " local dst = DST\n" - for bone, lineset in pairs(raw_track.linesets) do - if lineset.target_type ~= "bone" then - goto continue - end - local lineset_code = codegen_track( - raw_track, lineset, context.memoised, context.keysets, true) - - code = code .. "\n do" .. lineset_code .. "\n end\n" .. - " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" - ::continue:: - end - return code .. " end" -end - -local function codegen_object_target(raw_track, context) - local code = "\n if target.set_pos then\n" - code = code .. " local dst = DST\n" - local lineset = raw_track.linesets[""] - if not lineset then - return "" - end - local lineset_code = codegen_track( - raw_track, lineset, context.memoised, context.keysets, true) - code = code .. "\n do" .. lineset_code .. "\n end\n" - .. " set_matrix(target, dst)\n" - return code .. " end" -end - -local function codegen_camera_target(raw_track, context) - local code = "\n if target.set_zoom then\n" - code = code .. " local zoom = 1.0\n" - local lineset = raw_track.linesets[""] - if not lineset then - return "" - end - local lineset_code = codegen_track( - raw_track, lineset, context.memoised, context.keysets, false) - code = code .. "\n do" .. lineset_code .. "\n end\n" - .. " target:set_zoom(zoom)\n" - return code .. " end" -end - -function this.compile_track(raw_track, track_name) - local code = "" - local context = { - memoised = {}, - keysets = {}, - curves = {}, + maths = { + bezier_interpolation = bezier_interpolation } - for name, curve in pairs(raw_track.curves) do - context.curves[name] = load(string.format( - "return function(kl, kr, t) return %s end", - process_expression(curve.func, context.memoised) - ), "", "t", env)() - end - - code = code .. codegen_rig_target(raw_track, context) - code = code .. codegen_object_target(raw_track, context) - code = code .. codegen_camera_target(raw_track, context) - - local memoised_code = "" - for name, expression in pairs(context.memoised) do - memoised_code = memoised_code .. "\n local " .. name .. " = " - .. expression - end - - if #memoised_code > 0 then - code = memoised_code .. "\n" .. code - end - - local src = "return function(target, t, m)\n" - .. code .. "\nend" - - if this.TRACE_CODEGEN then - debug.log("[".. - string.escape(track_name or "nil").." codegen trace]:\n"..src) - end - - local generator, err = load( - src, "", "bt", table.extend({ - keysets = context.keysets, curves = context.curves - }, env)) - if not generator then - error(err) - end - return { - duration = raw_track.duration, - func = generator(), - } -end +} local loaded_tracks = {} local backup_tracks = {} diff --git a/res/modules/internal/animation_codegen.lua b/res/modules/internal/animation_codegen.lua new file mode 100644 index 000000000..3301f401f --- /dev/null +++ b/res/modules/internal/animation_codegen.lua @@ -0,0 +1,298 @@ +local internals = __vc_internals + +local INT_CONST = animation.INT_CONST +local INT_BEZIER = animation.INT_BEZIER + +local bezier_interpolation = animation.maths.bezier_interpolation + +local patterns = {} +local exclude_patters = { + "end", + (string.pattern_safe("'")), + (string.pattern_safe('"')), + (string.pattern_safe("--")), + (string.pattern_safe("..")), +} + +-- TODO: replace with actual expression -> lua translator +local function process_expression(src, memoised) + for i, pattern in ipairs(exclude_patters) do + if src:find(pattern) then + debug.print(exclude_patters) + error("invalid syntax "..string.escape(src)) + end + end + for i, pattern in ipairs(patterns) do + local pattern_safe = string.pattern_safe(pattern.pattern) + if src:find(pattern_safe) then + memoised[pattern.name] = pattern.pattern + src = src:gsub(pattern_safe, pattern.name) + end + end + return src +end + +local function key_neighbors(keys, frame) + local left = 1 + local right = #keys + + while left <= right do + local mid = math.floor((left + right) / 2) + + if keys[mid].frame < frame then + left = mid + 1 + elseif keys[mid].frame > frame then + right = mid - 1 + else + return mid, mid + end + end + if left > #keys then + left = #keys + end + return right, left +end + +local env = { + mat4 = mat4, + e = math.exp(1), + X = {1, 0, 0}, + Y = {0, 1, 0}, + Z = {0, 0, 1}, + DST = mat4.idt(), + value_at = function(keys, frame, interp) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + if interp == INT_CONST then + return left.value + end + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + if interp == INT_BEZIER then + return bezier_interpolation(left, right, t) + elseif type(interp) == "function" then + return interp(left.value, right.value, t) + end + return left.value * (1.0 - t) + right.value * t + end, + value_at_custom = function(keys, frame, func) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + return func(left, right, t) + end, + set_matrix = function(target, matrix) + local info = mat4.decompose(matrix) + if info then + local pos = info.translation + local rot = info.rotation + local scale = info.scale + if target.set_pos then + target:set_pos(pos) + end + if target.set_rot then + target:set_rot(rot) + end + if target.set_scale then + target:set_scale(scale) + end + end + end, + dump = debug.print +} + +local math_funcs = { + "sqrt", "min", "max", "deg", "rad", "log", "log10", "floor", "ceil", "sin", + "tan", "noise", "noise2", "sign", "round", "exp", "pi", "e" +} +for _, name in ipairs(math_funcs) do + env[name] = math[name] +end + +local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) + local lines = lineset.lines + local code = "" + local has_tsf = false + local translation = {false, false, false} + local rotation = {false, false, false} + local scale = {false, false, false} + for i, line in ipairs(lines) do + if line.expression then + code = code .. "\n local l" .. i .. " = (" .. + process_expression(line.expression, memoised) .. ")" + elseif line.keys then + local target_keysets = keysets[lineset.target_name] + if not target_keysets then + target_keysets = {} + keysets[lineset.target_name] = target_keysets + end + target_keysets[i] = line.keys + + if line.curve_func then + local valueat = string.format("curves[%s]", string.escape(line.curve_func)) + + code = code .. string.format( + "\n local l%d = value_at_custom(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, valueat) + else + code = code .. string.format( + "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, line.interp) + end + end + + if line.channel == animation.CH_TRANSLATE then + translation[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_ROTATE then + rotation[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_SCALE then + scale[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_ZOOM then + code = code .. "\n zoom = l" .. i + end + end + + if not has_tsf or not use_tsf then + return code + end + + code = code .. "\n mat4.idt(dst)" + if translation[1] or translation[2] or translation[3] then + code = code .. "\n mat4.translate(dst, {" .. + (translation[1] and ("l" .. translation[1]) or '0').. ", " .. + (translation[2] and ("l" .. translation[2]) or '0').. ", " .. + (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" + end + + local axis_names = {"X", "Y", "Z"} + local axis_indices = {X=1, Y=2, Z=3} + local rotation_order = raw_track.rotation_order + for i=1,3 do + local axis = axis_indices[rotation_order[i]] + local var = rotation[axis] + if var then + code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. + ", l" .. var .. ", dst)" + end + end + + if scale[1] or scale[2] or scale[3] then + code = code .. "\n mat4.scale(dst, {" .. + (scale[1] and ("l" .. scale[1]) or '1').. ", " .. + (scale[2] and ("l" .. scale[2]) or '1').. ", " .. + (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" + end + + return code +end + +local function codegen_rig_target(raw_track, context) + local code = "\n if target.set_matrix and target.index then\n" + code = code .. " local dst = DST\n" + for bone, lineset in pairs(raw_track.linesets) do + if lineset.target_type ~= "bone" then + goto continue + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, true) + + code = code .. "\n do" .. lineset_code .. "\n end\n" .. + " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" + ::continue:: + end + return code .. " end" +end + +local function codegen_object_target(raw_track, context) + local code = "\n if target.set_pos then\n" + code = code .. " local dst = DST\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, true) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " set_matrix(target, dst)\n" + return code .. " end" +end + +local function codegen_camera_target(raw_track, context) + local code = "\n if target.set_zoom then\n" + code = code .. " local zoom = 1.0\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, false) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " target:set_zoom(zoom)\n" + return code .. " end" +end + + +function internals.compile_animation_track(raw_track, track_name) + local code = "" + local context = { + memoised = {}, + keysets = {}, + curves = {}, + } + for name, curve in pairs(raw_track.curves) do + context.curves[name] = load(string.format( + "return function(kl, kr, t) return %s end", + process_expression(curve.func, context.memoised) + ), "", "t", env)() + end + + code = code .. codegen_rig_target(raw_track, context) + code = code .. codegen_object_target(raw_track, context) + code = code .. codegen_camera_target(raw_track, context) + + local memoised_code = "" + for name, expression in pairs(context.memoised) do + memoised_code = memoised_code .. "\n local " .. name .. " = " + .. expression + end + + if #memoised_code > 0 then + code = memoised_code .. "\n" .. code + end + + local src = "return function(target, t, m)\n" + .. code .. "\nend" + + if animation.TRACE_CODEGEN then + debug.log("[".. + string.escape(track_name or "nil").." codegen trace]:\n"..src) + end + + local generator, err = load( + src, "", "bt", table.extend({ + keysets = context.keysets, curves = context.curves + }, env)) + if not generator then + error(err) + end + return { + duration = raw_track.duration, + func = generator(), + } +end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 8146ffff6..051066ecd 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -106,7 +106,7 @@ end local function load_vca(source, filepath) local raw_track = parse_track(xml.parse_vcd(source, "track")) - return animation.compile_track(raw_track, filepath) + return internals.compile_animation_track(raw_track, filepath) end function internals.load_vca_animation(filepath, source, identifier) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index dee3bf579..3573bda07 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -334,6 +334,7 @@ world.raycast = entities.__world_raycast entities.__world_raycast = nil animation = require "core:animation" +require "core:internal/animation_codegen" require "core:internal/formats/vca" __vc_scripts_registry = require "core:internal/scripts_registry" From aa6ac5f5bd024fc21d7550f3c1caed10980de439 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 00:14:15 +0300 Subject: [PATCH 3/6] add animation.stop --- res/modules/animation.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index c42b15d25..fb0a915f2 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -68,13 +68,26 @@ end local running_actions = {} local playing_tracks = {} +local next_track_id = 1 function this.action(func) table.insert(running_actions, coroutine.create(func)) end function this.play(name, target) - table.insert(playing_tracks, {name=name, target=target, timer=0.0}) + local id = next_track_id + table.insert(playing_tracks, {id=id, name=name, target=target, timer=0.0}) + next_track_id = id + 1 + return id +end + +function this.stop(id) + for i, entry in pairs(playing_tracks) do + if entry.id == id then + table.remove(playing_tracks, i) + return + end + end end function internals.on_animation_frame() From 21bd5ba1b2fbccd3b9ce6297d583530ffee53a1a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 19:09:49 +0300 Subject: [PATCH 4/6] update animation api --- res/modules/animation.lua | 48 ++++++++++++++-------- res/modules/internal/animation_codegen.lua | 2 +- res/modules/internal/formats/vca.lua | 2 +- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index fb0a915f2..3f6acf351 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -68,26 +68,40 @@ end local running_actions = {} local playing_tracks = {} -local next_track_id = 1 function this.action(func) table.insert(running_actions, coroutine.create(func)) end -function this.play(name, target) - local id = next_track_id - table.insert(playing_tracks, {id=id, name=name, target=target, timer=0.0}) - next_track_id = id + 1 - return id -end - -function this.stop(id) - for i, entry in pairs(playing_tracks) do - if entry.id == id then - table.remove(playing_tracks, i) - return +local PlayingTrack = { + __index = { + stop = function(self) + table.remove_value(playing_tracks, self) + self.__timer = 0.0 + self.__playing = false + end, + pause = function(self) + table.remove_value(playing_tracks, self) + self.__playing = false + end, + resume = function(self) + if not self.__playing then + table.insert(playing_tracks, self) + self.__playing = true + end end - end + } +} + +function this.play(name, target) + local track = setmetatable({ + name = name, + target = target, + __timer = 0.0, + __playing = true, + }, PlayingTrack) + table.insert(playing_tracks, track) + return track end function internals.on_animation_frame() @@ -109,11 +123,11 @@ function internals.on_animation_frame() debug.error("animation track not found: "..track_info.name) table.remove(playing_tracks, i) else - track_info.timer = track_info.timer + delta - if track_info.timer > track.duration then + track_info.__timer = track_info.__timer + delta + if track_info.__timer > track.duration then table.remove(playing_tracks, i) else - track.func(track_info.target, track_info.timer) + track.func(track_info.target, track_info.__timer) end end end diff --git a/res/modules/internal/animation_codegen.lua b/res/modules/internal/animation_codegen.lua index 3301f401f..0b2cd7ce3 100644 --- a/res/modules/internal/animation_codegen.lua +++ b/res/modules/internal/animation_codegen.lua @@ -182,7 +182,7 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) local axis_names = {"X", "Y", "Z"} local axis_indices = {X=1, Y=2, Z=3} - local rotation_order = raw_track.rotation_order + local rotation_order = raw_track.rotation_order or "XYZ" for i=1,3 do local axis = axis_indices[rotation_order[i]] local var = rotation[axis] diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 051066ecd..fc910a8d7 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -24,7 +24,7 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end - raw_track.rotation_order = string.upper(node["rotation-order"]) or "XYZ" + raw_track.rotation_order = string.upper(node["rotation-order"]) end local function parse_curve(line, node) From 456d1c7b382bb0063258aa2695c8d002fa6c5e23 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 19:27:35 +0300 Subject: [PATCH 5/6] update doc/ru/vca.md --- doc/ru/vca.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/ru/vca.md b/doc/ru/vca.md index eb1f77b07..795541dc1 100644 --- a/doc/ru/vca.md +++ b/doc/ru/vca.md @@ -38,6 +38,18 @@ VCA файл состоит из набора директив, порядок Мета-информация применяется глобально. Поведение не зависит от её положения в файле, но рекомендуется располагать её в начале. +## Дополнительно + +- `curve` - объявляет пользовательский тип кривой. Указывается имя кривой `name` и выражение вычисления значения `func`. + +В выражении доступны два ключа `kl`, `kr` и значение `t` в диапазоне [0..1]. + +Пример: + +`@curve test-linear func (kl.value + (kr.value - kl.value) * t)` + +Пользовательские кривые указываются с префиксом `.`. Пример: `@move by y curve .test-linear {...}`. + ## Кривые с ключевыми кадрами Требуется через атрибут `curve` указать тип кривой: From ca9ff7ba7e5afe97fc8aa1af57c86e93c99fa9c3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Sep 2026 15:27:21 +0300 Subject: [PATCH 6/6] fix missing default rotation-order value --- res/modules/internal/formats/vca.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index fc910a8d7..f06c99a7b 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -24,7 +24,7 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end - raw_track.rotation_order = string.upper(node["rotation-order"]) + raw_track.rotation_order = string.upper(node["rotation-order"] or "XYZ") end local function parse_curve(line, node)