diff --git a/bin/hidekit.css b/bin/hidekit.css index 58bd311d6..7adc7220b 100644 --- a/bin/hidekit.css +++ b/bin/hidekit.css @@ -43,8 +43,225 @@ kit-root { max-height: 500px; overflow: auto; } -} + kit-div.kit-param-comp > kit-text { + flex: 0 0 auto; + min-width: 10px; + margin-left: 3px; + } + + /* the first widget of a line shared with others: the same width on every such line */ + kit-div.kit-line-first > :not(kit-label) { + flex: 0 0 90px; + } + kit-div.kit-line-first ~ kit-div > .gradient-box { + flex: 1 1 0; + min-width: 0; + } + + kit-curve-editor { + flex: 1 1; + display: flex; + min-width: 0; + height: 22px; + cursor: pointer; + + > svg { + flex: 1 1; + min-width: 0; + width: 0; + padding: 2px; + overflow: visible; + } + > svg path.main { + stroke-width: 1.5px; + } + &:hover > svg { + border-color: var(--color-tool-bg-hover); + } + } + kit-curve-editor, kit-curve-editor-window { + svg { + box-sizing: border-box; + background-color: #161616; + border: 1px solid var(--color-border); + border-radius: 3px; + } + path { + fill: none; + stroke: #f05a5a; + } + path, line { + vector-effect: non-scaling-stroke; + } + path.main { + stroke-width: 2px; + } + path.extreme { + stroke-width: 1px; + stroke-dasharray: 4 3; + } + .band { + fill: rgba(240, 90, 90, 0.15); + stroke: none; + } + line.grid { + stroke: var(--color-border); + } + line.zero { + stroke: #555; + } + rect.outside { + fill: rgba(0, 0, 0, 0.35); + } + rect.keys-box { + fill: rgba(240, 181, 90, 0.12); + stroke: #f0b55a; + stroke-dasharray: 3 2; + pointer-events: none; + } + rect.selection-box { + fill: rgba(240, 181, 90, 0.06); + stroke: #f0b55a; + } + text { + fill: var(--color-highlight-gray); + font-size: 9px; + pointer-events: none; + user-select: none; + } + rect.key { + fill: #eee; + cursor: move; + } + rect.key.selected { + fill: #f0b55a; + } + path.other { + opacity: 0.5; + } + rect.key.other { + fill: var(--color-highlight-gray); + cursor: pointer; + } + line.tangent { + stroke: #999; + } + circle.handle { + fill: #999; + cursor: move; + } + } + + kit-curve-editor-window { + flex: 1 1; + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + font-size: 11px; + color: var(--color-text); + + kit-curve-body { + flex: 1 1; + display: flex; + flex-direction: column; + gap: 6px; + min-height: 0; + padding: 8px; + } + + kit-root.kit-curve-fields-root { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + } + kit-line.kit-curve-fields { + grid-column: 2; + flex-wrap: wrap; + max-width: none; + margin-block: 0; + padding: 2px 4px; + border-radius: 3px; + + &.kit-curve-selectable { + cursor: pointer; + &:hover:not(.active) { + background-color: hsl(from var(--color-menu-bg-highlight) h s l / 0.5); + } + } + &.active { + background-color: var(--color-menu-bg-highlight); + } + kit-text { + flex: 0 0 auto; + min-width: 28px; + } + > kit-div { + flex: 0 0 auto; + } + kit-select { + min-width: 120px; + } + kit-slider { + width: 64px; + } + kit-button { + flex: 0 0 auto; + width: 24px; + } + > kit-spacer { + flex: 1 1; + } + } + kit-curve-view { + flex: 1 1; + display: flex; + min-height: 0; + outline: none; + + > svg { + flex: 1 1; + min-width: 0; + } + } + kit-presets { + flex: 0 0 auto; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + padding: 6px 8px; + background-color: var(--color-menu-bg); + border-top: 1px solid var(--color-border); + + svg { + width: 44px; + height: 28px; + padding: 3px; + overflow: visible; + background-color: var(--color-menu-bg); + cursor: pointer; + } + svg:hover { + border-color: var(--color-tool-bg-hover); + background-color: var(--color-menu-bg-highlight); + } + } + kit-curve-hint { + margin-left: auto; + color: var(--color-highlight-gray); + } + } + + .kit-unset:not(kit-div), kit-div.kit-unset > * { + opacity: 0.45; + filter: grayscale(1); + } + .kit-unset:not(kit-div):hover, kit-div.kit-unset > :hover, kit-div.kit-unset > :focus-within { + opacity: 0.8; + } +} /** We use scope to avoid messing with the inner styles of complex widgets **/ @@ -71,6 +288,8 @@ kit-root { --widget-bg-disabled: #222222; --widget-border-color: #484848; --widget-border-color-disabled: #333333; + --widget-bg-animated: #2c3d26; + --widget-border-color-animated: #4f7a3a; --widget-border-color-hover: hsl(from var(--widget-border-color) h s calc(l + 20)); --widget-left-pad: 5px; @@ -87,6 +306,10 @@ kit-root { --section-padding: 8px; + /* the label of a line, and the most its widgets take after it (the rest of the panel stays empty) */ + --label-width: 120px; + --content-max-width: 260px; + /* needed for range background */ --range-fill: var(--button-bg-highlight); @@ -202,6 +425,15 @@ kit-root { } } + .kit-animated.disabled { + pointer-events: auto; + + input { + background-color: var(--widget-bg-animated); + border-color: var(--widget-border-color-animated); + } + } + --basic-border: 1px solid; kit-button, input { @@ -312,6 +544,9 @@ kit-root { font-variant-numeric: tabular-nums; } > kit-line { + /* the header of a collapsible item (list) spans the panel */ + max-width: none; + &::before { display: block; content: ""; @@ -588,6 +823,8 @@ kit-root { gap: 4px; min-width: 0; min-height: var(--widget-height); + /* the label, the gap after it, the widgets, the paddings */ + max-width: calc(var(--label-width) + 4px + var(--content-max-width) + var(--section-padding) + 2px); kit-label { font-variant-numeric: tabular-nums; @@ -621,7 +858,7 @@ kit-root { } .first { - flex: 0 0 85px; + flex: 0 0 var(--label-width); text-align: right; margin-left: 0; } diff --git a/bin/style.css b/bin/style.css index bdd6ca363..eb6b694b4 100644 --- a/bin/style.css +++ b/bin/style.css @@ -1776,33 +1776,6 @@ input[type=checkbox] { /* background-color: rgba(0, 0, 0, 0.219); */ outline: none; } - .keyPopup { - padding: 4px; - padding-bottom: 2px; - .line { - display: flex; - align-items: baseline; - margin-bottom: 2px; - } - label{ - text-align: right; - padding-right: 0.5em; - width: 40px; - display: inline-block; - } - input, select { - box-sizing: border-box; - height: 22px !important; - width: 100px !important; - margin: 0; - padding: 2px; - } - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - } svg { --selectCol: rgb(255, 255, 255); --previewCol: rgb(192, 111, 6); @@ -1886,6 +1859,34 @@ input[type=checkbox] { } } +.keyPopup { + padding: 4px; + padding-bottom: 2px; + .line { + display: flex; + align-items: baseline; + margin-bottom: 2px; + } + label{ + text-align: right; + padding-right: 0.5em; + width: 40px; + display: inline-block; + } + input, select { + box-sizing: border-box; + height: 22px !important; + width: 100px !important; + margin: 0; + padding: 2px; + } + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } +} + /* FX Editor */ .fx-script { diff --git a/extraParams.hxml b/extraParams.hxml index b2b058554..f297b7ecf 100644 --- a/extraParams.hxml +++ b/extraParams.hxml @@ -1 +1,2 @@ ---macro include("hide.kit") \ No newline at end of file +--macro include("hide.kit") +-D fx-migration \ No newline at end of file diff --git a/hide/kit/Category.hx b/hide/kit/Category.hx index f2b9cae1e..6241a0477 100644 --- a/hide/kit/Category.hx +++ b/hide/kit/Category.hx @@ -95,14 +95,17 @@ class Category extends Widget> { #end } - override function getEditMenuContent() : Array { - var content = super.getEditMenuContent(); + override function getEditMenuContent(?target: NativeElement) : Array { + var content = super.getEditMenuContent(target); content.unshift({isSeparator: true}); content.unshift({label: "Collapse", click: collapse}); content.unshift({label: "Collapse All", click: root.collapse.bind()}); return content; } + override function getEditMenuItems(out: Array, ?target: NativeElement) { + } + override function getChildDisabled():Bool { return isDisabled() || value == false; } diff --git a/hide/kit/Curve.hx b/hide/kit/Curve.hx index b71bcc0eb..a609c77bb 100644 --- a/hide/kit/Curve.hx +++ b/hide/kit/Curve.hx @@ -2,50 +2,451 @@ package hide.kit; #if domkit +import hrt.impl.Curve.CurveKeyMode; +#if js +import hide.comp.curveeditor.CurveEditor; + +typedef CurveWindow = { + var sub : hide.comp.SubWindow; + var edited : Curve; + var content : CurveEditor; + var fields : KitRoot; +} +#end + +typedef TrackSpread = { + min : Float, + max : Float +}; + +typedef CopiedTrack = { + curve : Dynamic, + spread : TrackSpread +}; + +typedef CurveBand = { + a : CurveTrack, + b : CurveTrack +}; + +typedef CurvePreset = { + name : String, + keys : Array<{ t : Float, v : Float, ?slope : Float }> +}; + +@:access(hrt.prefab.Prefab) +@:access(hide.kit.KitRoot) class Curve extends Element { - public var value(default, set) : hrt.prefab.Curve; - var curveEditor: #if hui hrt.ui.HuiCurveBox #elseif js hide.comp.CurveEditor #else NativeElement #end; + public var maxTime = 1.; + public var label : String; + public var tracks : Array = []; + public var bands : Array = []; - function set_value(v: hrt.prefab.Curve) : hrt.prefab.Curve { - value = v; - refreshCurve(); - return value; - } + public static var PRESETS : Array = [ + { name: "Constant", keys: [{ t: 0, v: 1 }, { t: 1, v: 1 }] }, + { name: "Linear", keys: [{ t: 0, v: 0 }, { t: 1, v: 1 }] }, + { name: "Linear, decreasing", keys: [{ t: 0, v: 1 }, { t: 1, v: 0 }] }, + { name: "Ease in", keys: [{ t: 0, v: 0, slope: 0 }, { t: 1, v: 1, slope: 2 }] }, + { name: "Ease out", keys: [{ t: 0, v: 0, slope: 2 }, { t: 1, v: 1, slope: 0 }] }, + { name: "Ease in out", keys: [{ t: 0, v: 0, slope: 0 }, { t: 1, v: 1, slope: 0 }] }, + { name: "Bell", keys: [{ t: 0, v: 0, slope: 0 }, { t: 0.5, v: 1, slope: 0 }, { t: 1, v: 0, slope: 0 }] }, + { name: "Fade in out", keys: [{ t: 0, v: 0 }, { t: 0.2, v: 1 }, { t: 0.8, v: 1 }, { t: 1, v: 0 }] }, + ]; + + #if js + static inline var WINDOW_BOUNDS_KEY = "curveEditorWindow"; + static var win : CurveWindow; - public function new(parent: Element, id: String, value: hrt.prefab.Curve) : Void { + var preview : hide.comp.curveeditor.CurveThumbnail; + #end + + public function new(parent: Element, id: String, label: String) { super(parent, id); - this.value = value; + this.label = label; } - override function makeSelf() : Void { + public function refresh() { #if js - var ctx = @:privateAccess (cast root.editor : hide.prefab.EditContext.HideJsEditContext2).ctx; - var container = new hide.Element('
'); - - curveEditor = new hide.comp.CurveEditor(ctx.properties.undo, container, false); - setupPropLine(null, container[0], false); - - var resizeObserver = new hide.comp.ResizeObserver((_, _) -> refreshCurve()); - resizeObserver.observe(container[0]); - #elseif hui - curveEditor = new hrt.ui.HuiCurveBox(); - setupPropLine(null, curveEditor, false); - refreshCurve(); + preview?.refresh(); + if (win?.edited == this) { + win.content.refresh(); + win.fields.refreshFields(); + } #end } - function refreshCurve() { + public function addTrack(curve: hrt.impl.Curve, ?spread: TrackSpread) : CurveTrack { + var t = new CurveTrack(curve, spread, this); + tracks.push(t); + return t; + } + + public function addBand(a: CurveTrack, b: CurveTrack) { + bands.push({ a: a, b: b }); + } + + public static function getCurveFromPreset(p: CurvePreset) : hrt.impl.Curve { + var c = new hrt.impl.Curve(); + for (i => k in p.keys) { + var key = c.addKey(k.t, k.v, k.slope == null ? CurveKeyMode.Linear : CurveKeyMode.Aligned); + if (k.slope != null) { + var prev = (k.t - (p.keys[i - 1]?.t ?? k.t - 0.3)) / 3, next = ((p.keys[i + 1]?.t ?? k.t + 0.3) - k.t) / 3; + key.prevHandle = new hrt.impl.Curve.CurveHandle(-prev, -prev * k.slope); + key.nextHandle = new hrt.impl.Curve.CurveHandle(next, next * k.slope); + } + } + return c; + } + + override function canCopy() { + return true; + } + + override function canPaste() { + return true; + } + + override function copySelf(target: Dynamic) { + var copied : Array = []; + for (t in tracks) { + copied.push({ + curve: @:privateAccess t.data?.copyToDynamic(null), + spread: Reflect.copy(t.spread), + }); + } + target.tracks = copied; + } + + override function pasteSelf(data: Dynamic) { + var copied : Array = data.tracks; + if (copied == null) + return; + root.editor.beginMultiUndo(); + for (i => t in tracks) { + var c = copied[i]; + if (c == null) + continue; + t.edit(() -> { + if (c.curve != null && t.data != null) { + var keys = new hrt.impl.Curve(); + @:privateAccess keys.copyFromDynamic(hrt.prefab.Diff.deepCopy(c.curve)); + t.data.keys = keys.keys; + } + if (c.spread != null && t.spread != null) { + t.spread.min = c.spread.min; + t.spread.max = c.spread.max; + } + }, false); + } + root.editor.finishMultiUndo(); + } + + override function makeSelf() { #if js - if (curveEditor != null && value != null && curveEditor.element.width() > 0) { - curveEditor.saveDisplayKey = value.getAbsPath(true); - curveEditor.curves = [value]; - curveEditor.refresh(); - } - #elseif hui - if (curveEditor != null) - curveEditor.value = value; + var e = js.Browser.document.createElement("kit-curve-editor"); + preview = new hide.comp.curveeditor.CurveThumbnail(e, tracks, bands, maxTime); + e.title = "Edit the curve"; + e.addEventListener("click", (_) -> if (!isDisabled()) openWindow()); + setupPropLine(null, cast e, false); + if (win != null && sameAs(win.edited)) + bindWindow(); + #else + super.makeSelf(); #end } + + #if js + function sameAs(other: Curve) { + return other.root.prefab == root.prefab && other.getIdPath() == getIdPath(); + } + + function openWindow() { + if (win == null) + createWindow(); + bindWindow(); + win.sub.focus(); + } + + function bindWindow() { + var previous = win.edited != null && sameAs(win.edited) ? win.content : null; + win.edited = this; + win.sub.anchor = native.get(); + win.sub.title = label; + var content = js.Browser.document.createElement("kit-curve-editor-window"); + win.sub.setContent(content); + + var body = js.Browser.document.createElement("kit-curve-body"); + content.appendChild(body); + + // the widgets of the window edit the prefab of the inspector, apart from its tree + var fields = new KitRoot(null, null, root.prefab, root.editor); + fields.header = false; + fields.classList.push("kit-curve-fields-root"); + win.fields = fields; + var view : CurveEditor = null; + var rows = []; + for (i => t in tracks) { + var line = new Line(fields, "track" + i); + line.full = true; + line.classList.push("kit-curve-fields"); + if (tracks.length > 1) + line.classList.push("kit-curve-selectable"); + rows.push(line); + if (t.name != null) + new Text(line, "name", t.name); + if (t.data != null) + for (f in [{ id: "flipTime", text: "↔", tooltip: "Flip the curve in time", flip: t.flipTime }, { id: "flipValue", text: "↕", tooltip: "Flip the curve in value", flip: t.flipValue }]) { + var button = new Button(line, f.id, f.text); + button.tooltip = f.tooltip; + // the flips record their own undo step + button.noUndo = true; + button.onClick = f.flip; + } + new Spacer(line, "push"); + if (t.spread != null) { + // the random half width where the curve is the lowest / the highest, the band follows the value of the curve in between + function spreadSlider(id: String, label: String, tooltip: String, get: () -> Float, set: Float -> Void) { + var s = new Slider(line, id); + s.label = label; + s.tooltip = tooltip; + s.decimals = 3; + // the edit records its own undo step + s.noUndo = true; + s.value = get(); + @:privateAccess s.getFieldValue = get; + s.onValueChange = (temp) -> { + // typing sends the unchanged value: no edit, that would refresh the field being typed + if (s.value == get()) { + if (!temp) + t.edit(null, false); + return; + } + t.edit(() -> set(s.value), temp); + } + } + spreadSlider("spreadLow", "Rand min", "Random spread (±) at min point", () -> t.spread.min, v -> t.spread.min = v); + spreadSlider("spreadHigh", "Rand max", "Random spread (±) at max point", () -> t.spread.max, v -> t.spread.max = v); + } + } + fields.make(); + body.appendChild(fields.native.get()); + + for (i => row in rows) { + if (tracks.length > 1) { + row.native.get().addEventListener("pointerdown", (_) -> { + view.setTrack(tracks[i]); + view.refresh(); + }); + } + } + + var area = js.Browser.document.createElement("kit-curve-view"); + area.tabIndex = 0; + body.appendChild(area); + var active = previous == null ? 0 : previous.editor.tracks.indexOf(previous.track); + view = new CurveEditor(this, active < tracks.length ? active : 0); + win.content = view; + if (previous != null) + view.keepView(previous); + area.appendChild(view.svg); + area.addEventListener("keydown", view.onKeyDown); + + var presets = js.Browser.document.createElement("kit-presets"); + content.appendChild(presets); + function fillPresets() { + presets.innerHTML = ""; + for (i => row in rows) + row.native.toggleClass("active", tracks.length > 1 && tracks[i] == view.track); + if (view.track.data == null) { + var hint = js.Browser.document.createElement("kit-curve-hint"); + hint.innerText = "A missing curve, or not a plain one: it is not edited here"; + presets.appendChild(hint); + return; + } + for (p in PRESETS) { + var thumb = new hide.comp.curveeditor.CurveThumbnail(presets, [new CurveTrack(getCurveFromPreset(p), null, null)], [], 1); + thumb.element.on("click", (_) -> view.track.applyPreset(p)); + } + } + + fillPresets(); + view.onTrackChange = fillPresets; + win.sub.window.requestAnimationFrame((_) -> view.refresh()); + area.focus(); + } + + static function createWindow() { + var sub = new hide.comp.SubWindow(640, 400, WINDOW_BOUNDS_KEY); + win = { sub: sub, edited: null, content: null, fields: null }; + sub.onResize = () -> win.content.refresh(); + sub.onKeyDown = onWindowKeyDown; + sub.onClose = () -> win = null; + } + + static function onWindowKeyDown(e: js.html.KeyboardEvent) { + var target : js.html.Element = cast e.target; + if (!e.ctrlKey || target.tagName == "INPUT") + return; + var native = win.edited.native.get(); + var view = Lambda.find(@:privateAccess hide.Ide.inst.views, v -> v.element[0].contains(native)); + view?.processKeyEvent(new js.jquery.Event(cast e)); + } + #end + + public dynamic function onValueChange(temporaryEdit: Bool) : Void {} + + public dynamic function onSpreadChange(spread: TrackSpread) : Void {} +} + +@:access(hrt.prefab.Prefab) +@:access(hide.kit.Element) +@:access(hrt.prefab.EditContext2) +class CurveTrack { + public var editor(default, null) : Curve; + + public var name : String; + public var data : hrt.impl.Curve; + public var spread : TrackSpread; + + var editStart : Dynamic; + + public function new(data: hrt.impl.Curve, spread: TrackSpread, editor: Curve) { + this.editor = editor; + this.spread = spread; + this.data = data; + } + + public function edit(f: () -> Void, temp: Bool) { + if (f == null && editStart == null) + return; + var before = editStart ??= keys(); + var after : Dynamic = null; + function apply(isUndo: Bool) { + if (after == null) + after = keys(); + else if (data != null) + @:privateAccess data.copyFromDynamic(hrt.prefab.Diff.deepCopy(isUndo ? before : after)); + rebuild(); + } + + var unchanged = f == null && haxe.Json.stringify(before) == haxe.Json.stringify(keys()); + editor.change({ + callback: () -> { + var min = spread?.min, max = spread?.max; + if (f != null) + f(); + if (spread != null && (spread.min != min || spread.max != max)) + editor.onSpreadChange(spread); + editor.onValueChange(temp); + }, + isTemporaryEdit: temp, + recordUndo: true, + sideEffects: unchanged ? null : apply, + }); + if (!temp) + editStart = null; + } + + public function applyPreset(p: CurvePreset) { + edit(() -> data.keys = Curve.getCurveFromPreset(p).keys, false); + } + + function rangePos(v: Float) : Float { + if (data == null) + return 0.; + var r = data.getValueRange(); + return r.max - r.min <= 1e-9 * Math.max(1, Math.abs(r.max)) ? 0. : (v - r.min) / (r.max - r.min); + } + + public function spreadAt(time: Float) : Float { + return spread == null ? 0. : hxd.Math.lerp(spread.min, spread.max, rangePos(data?.getVal(time) ?? 0.)); + } + + public function moveEdge(time: Float, edge: Int, value: Float) { + if (spread == null) + return; + var v = data?.getVal(time) ?? 0.; + var half = Math.max(edge * (value - v), 0); + var n = rangePos(v); + if (n < 0.5) + spread.min = round(Math.max((half - spread.max * n) / (1 - n), 0)); + else + spread.max = round(Math.max((half - spread.min * (1 - n)) / n, 0)); + } + + static inline function round(v: Float) return Math.round(v * 1e6) / 1e6; + + public function flipTime() { + var end = Math.isFinite(editor.maxTime) ? editor.maxTime : data.keys[data.keys.length - 1].time; + edit(() -> { + for (k in data.keys) { + k.time = end - k.time; + var h = k.prevHandle; + k.prevHandle = k.nextHandle; + k.nextHandle = h; + for (h in [k.prevHandle, k.nextHandle]) + if (h != null) + h.dt = -h.dt; + } + data.keys.reverse(); + }, false); + } + + public function flipValue() { + edit(() -> { + var min = Math.POSITIVE_INFINITY, max = Math.NEGATIVE_INFINITY; + for (k in data.keys) { + min = Math.min(min, k.value); + max = Math.max(max, k.value); + } + for (k in data.keys) { + k.value = min + max - k.value; + for (h in [k.prevHandle, k.nextHandle]) + if (h != null) + h.dv = -h.dv; + } + if (spread != null) { + var v = spread.min; + spread.min = spread.max; + spread.max = v; + } + }, false); + } + + public function fixKey(k: hrt.impl.Curve.CurveKey) { + var i = data.keys.indexOf(k); + var prev = data.keys[i - 1], next = data.keys[i + 1]; + switch (k.mode) { + case Aligned, Free: + k.prevHandle ??= new hrt.impl.Curve.CurveHandle(prev != null ? (prev.time - k.time) / 3 : -0.1, 0); + k.nextHandle ??= new hrt.impl.Curve.CurveHandle(next != null ? (next.time - k.time) / 3 : 0.1, 0); + case Linear, Constant: + k.prevHandle = null; + k.nextHandle = null; + } + } + + public function deleteKeys(keys: Array) { + if (keys.length == 0 || data.keys.length - keys.length < 1) + return; + edit(() -> for (k in keys) data.keys.remove(k), false); + } + + function keys() : Dynamic { + return data == null ? null : @:privateAccess data.copyToDynamic(null); + } + + function rebuild() { + editor.root.editor.rebuildPrefab(@:privateAccess editor.root.prefab); + function visit(e: Element) { + var p = Std.downcast(e, Curve); + if (p != null && Lambda.exists(p.tracks, t -> t.data == data)) + p.refresh(); + for (c in e.children) + visit(c); + } + visit(editor.root); + } } -#end \ No newline at end of file +#end diff --git a/hide/kit/Element.hx b/hide/kit/Element.hx index b9b613611..92892761d 100644 --- a/hide/kit/Element.hx +++ b/hide/kit/Element.hx @@ -69,6 +69,11 @@ class Element { **/ public var disabled(default, set) = false; + /** + If set, the element is shown grayed out but stays editable + **/ + public var unset(default, set) = false; + /** If set, the element will take width units of space in its line **/ @@ -79,6 +84,15 @@ class Element { **/ public var tooltip : String = null; + /** + The css classes added to the element + **/ + public var classList : Array = []; + + /** + Items added to the context menu of this element (collected recursively with its children) + **/ + public var menuItems : () -> Array = null; /** Internal ID of this element that is unique between this element siblings (use getIdPath for a unique identifier in this element tree) @@ -114,6 +128,12 @@ class Element { return disabled; } + function set_unset(v: Bool) { + unset = v; + native?.toggleClass("kit-unset", v); + return unset; + } + function isDisabled() { if (parent != null) return disabled || parent.getChildDisabled(); @@ -155,6 +175,9 @@ class Element { // call setters disabled = disabled; + unset = unset; + for (c in classList) + native?.addClass(c); makeChildren(); @@ -250,13 +273,36 @@ class Element { } } - function getEditMenuContent() : Array { - return [ + function getEditMenuItems(out: Array, ?target: NativeElement) { + if (menuItems != null) { + var items = menuItems(); + if (items.length > 0 && out.length > 0 && out[out.length - 1].isSeparator != true && items[0].isSeparator != true) + out.push({ isSeparator: true }); + for (item in items) + out.push(item); + } + var from = children; + #if js + if (target != null) + from = from.filter(c -> c.native != null && c.native.get().contains(target)); + #end + for (c in from) + c.getEditMenuItems(out, target); + } + + function getEditMenuContent(?target: NativeElement) : Array { + var items = []; + getEditMenuItems(items, target); + if (isDisabled()) + return items; + if (items.length > 0) + items.push({isSeparator: true}); + return items.concat([ {label: "Copy", click: copyToClipboard, enabled: canCopy()}, {label: "Paste", click: pasteFromClipboard, enabled: canPaste()}, {isSeparator: true}, {label: "Reset", click: resetWithUndo, enabled: canReset()} - ]; + ]); } // Internal API @@ -275,25 +321,29 @@ class Element { } function addEditMenu(el: NativeElement) { - if (!disabled) { #if js el.get().addEventListener("contextmenu", (e: js.html.MouseEvent) -> { if ((cast e.target:js.html.Element).closest(".is-cdb-editor") != null) return; + var content = getEditMenuContent(cast e.target); + if (content.length == 0) + return; e.preventDefault(); e.stopPropagation(); - hide.comp.ContextMenu.createFromEvent(e, getEditMenuContent()); + hide.comp.ContextMenu.createFromEvent(e, content); }); #elseif hui var element = Std.downcast(el.get(), hrt.ui.HuiElement); element?.onClick = (e) -> { if (e.button != hxd.Key.MOUSE_RIGHT) return; - element.uiBase.contextMenu(getEditMenuContent()); + var content = getEditMenuContent(); + if (content.length == 0) + return; + element.uiBase.contextMenu(content); } #end - } } function canCopy() : Bool { @@ -457,6 +507,7 @@ class Element { // reset values are broadcasted as temporary edits, commit them in the multi undo @:privateAccess root.finishUndoPoint(); root.editor.finishMultiUndo(); + root.editor.rebuildInspector(); } /** @@ -676,8 +727,11 @@ class Element { new hide.kit.Texture(this, def.name); case PColor: var color = new hide.kit.Color(this, def.name); - color.arr = true; - defValue = [0,0,0,1]; + color.arr = !Std.isOfType(Reflect.field(props, def.name), Int); + if (color.arr) + defValue = [0,0,0,1]; + else + color.alpha = true; // an int is ARGB color; case PGradient: new hide.kit.Gradient(this, def.name); @@ -696,6 +750,7 @@ class Element { } else { var line = new Line(this, def.name); line.label = def.disp ?? hide.kit.Macros.camelToSpaceCase(def.name); + line.tooltip = def.desc; var vec : Array = Reflect.field(props, def.name); if (vec == null) { vec = []; @@ -735,6 +790,10 @@ class Element { } if (widget != null) { widget.label = def.disp ?? hide.kit.Macros.camelToSpaceCase(def.name); + widget.tooltip = def.desc; + widget.anim = def.name; + if (def.def != null) + widget.defaultValue = def.def; widget.value = Reflect.field(props, def.name) ?? defValue; @:privateAccess widget.onFieldChange = (_) -> { Reflect.setField(props, def.name, widget.value); diff --git a/hide/kit/KitRoot.hx b/hide/kit/KitRoot.hx index ef5aa3772..f8c96f272 100644 --- a/hide/kit/KitRoot.hx +++ b/hide/kit/KitRoot.hx @@ -5,10 +5,12 @@ package hide.kit; class KitRoot #if !macro extends Element #end { #if !macro public var editedPrefabsProperties : Array = []; - var prefab : hrt.prefab.Prefab; - var prefabUndoPoint : Dynamic = null; public var editor(default, null) : hrt.prefab.EditContext2; public var isMultiEdit(default, null) : Bool; + public var header = true; + var prefab : hrt.prefab.Prefab; + var prefabUndoPoint : Dynamic = null; + var animated : Array> = []; public function new(parent: Element, id: String, prefab: hrt.prefab.Prefab, editor: hrt.prefab.EditContext2) { super(parent, id); @@ -20,6 +22,8 @@ class KitRoot #if !macro extends Element #end { override function makeSelf() : Void { #if js native = js.Browser.document.createElement("kit-root"); + if (!header) + return; var title = js.Browser.document.createElement("kit-title"); var titleText = prefab.getHideProps().name; @@ -153,25 +157,83 @@ class KitRoot #if !macro extends Element #end { var after = hrt.prefab.Diff.deepCopy(prefab.save()); if (hrt.prefab.Diff.diff(before, after) != Skip) { sideEffects.push((isUndo) -> { - var data = hrt.prefab.Diff.deepCopy(isUndo ? before : after); - #if (editor || editor_hl) - prefab.editorTempLoad(data); - #else - prefab.load(data); - #end + hrt.prefab.EditContext2.tempLoad(prefab, hrt.prefab.Diff.deepCopy(isUndo ? before : after)); doTry(() -> prefab.updateInstance()); }); } } public function postEditStep() { + if (prefab == null) + return; + + if (!isMultiEdit) + registerAnimatedField(this); + #if castle - if (prefab != null) { - new CDB(this, "cdb"); - } + new CDB(this, "cdb"); #end } + public function refreshAnimatedFields() { + for (a in animated) + a.refreshFields(); + } + + function registerAnimatedField(e: Element) { + for (c in e.children) { + var widget : Widget = Std.downcast(c, Widget); + if (widget?.anim != null) { + var curve = hrt.prefab.Curve.getCurve(prefab, widget.anim); + if (curve != null) { + var value = curve.makeVal(); + @:privateAccess widget.getFieldValue = () -> hxd.Math.fmt(evaluateAnimation(value)); + widget.value = hxd.Math.fmt(evaluateAnimation(value)); + + widget.disabled = true; + widget.classList.push("kit-animated"); + addMenuItem(widget, { label: "Delete animation", click: () -> deleteAnimation(curve) }); + animated.push(widget); + } else if (hrt.prefab.Curve.getCurve(prefab, widget.anim, false) == null) { + addMenuItem(widget, { label: "Animate", click: () -> animate(widget.anim, widget.value) }); + } + } + registerAnimatedField(c); + } + } + + function addMenuItem(widget: Widget, item: hrt.ui.HuiMenu.MenuItem) { + var menuItems = widget.menuItems; + widget.menuItems = () -> { + var items : Array = menuItems == null ? [] : menuItems().concat([{ isSeparator: true }]); + items.push(item); + return items; + } + } + + function evaluateAnimation(value: hrt.prefab.fx.Value) : Float { + var root = prefab.getRoot(); + var anim3d = Std.downcast(root.to(hrt.prefab.Object3D)?.local3d, hrt.prefab.fx.FX.FXAnimation); + if (anim3d != null) + return @:privateAccess anim3d.evaluator.getFloat(value, anim3d.localTime); + var anim2d = Std.downcast(root.to(hrt.prefab.Object2D)?.local2d, hrt.prefab.fx.FX2D.FX2DAnimation); + return @:privateAccess anim2d.evaluator.getFloat(value, anim2d.localTime); + } + + function animate(name: String, value: Float) { + editor.recordPrefabUndoWithChildren(prefab, () -> { + var curve = new hrt.prefab.fx.Curve(prefab, prefab.shared); + curve.name = name; + curve.addKey(0, value); + }); + editor.rebuildInspector(); + } + + function deleteAnimation(curve: hrt.prefab.Curve) { + editor.recordPrefabUndoWithChildren(prefab, () -> curve.remove()); + editor.rebuildInspector(); + } + #end } diff --git a/hide/kit/Line.hx b/hide/kit/Line.hx index 7f92235d4..cfb63a9cc 100644 --- a/hide/kit/Line.hx +++ b/hide/kit/Line.hx @@ -3,14 +3,22 @@ package hide.kit; #if domkit class Line extends Element { - @:attr public var label: String; - @:attr public var multiline: Bool = false; - @:attr public var full: Bool = false; + public var label : String; + public var multiline : Bool = false; + public var full : Bool = false; + public var stealLabel : Bool = true; var decorationsLeft: Array = null; public var labelElement: NativeElement; public var multilineElement: NativeElement; + + override function set_unset(v: Bool) { + unset = v; + labelElement?.toggleClass("kit-unset", v); + return unset; + } + override function get_nativeContent():NativeElement { return multilineElement ?? native; } @@ -43,7 +51,7 @@ class Line extends Element { labelElement.addChild(span); } - if (!multiline) { + if (!multiline && stealLabel) { stealChildLabel(labelElement); } } diff --git a/hide/kit/Slider.hx b/hide/kit/Slider.hx index a550c9a0c..82ec837c3 100644 --- a/hide/kit/Slider.hx +++ b/hide/kit/Slider.hx @@ -61,7 +61,7 @@ class Slider extends Widget { var slider = null; #end - var showRange: Bool = false; + public var showRange: Bool = false; var subPixelSlide : Float = 0; var startValueLinear : Float = 0; var expScale : Null = null; diff --git a/hide/kit/Widget.hx b/hide/kit/Widget.hx index 0b0e80882..78469d4ce 100644 --- a/hide/kit/Widget.hx +++ b/hide/kit/Widget.hx @@ -17,6 +17,9 @@ abstract class Widget extends Element { /** If set, modifying the value don't save an undo step (use ctx.recordUndo to handle that yourself))**/ public var noUndo: Bool = false; + /** If set, the name of the animation curve of this field in an FX **/ + public var anim : String = null; + var fieldName: String; var input: NativeElement; diff --git a/hide/tools/migrate/Cli.hx b/hide/tools/migrate/Cli.hx new file mode 100644 index 000000000..7c934d16b --- /dev/null +++ b/hide/tools/migrate/Cli.hx @@ -0,0 +1,119 @@ +package hide.tools.migrate; + +#if fx_migration + +import hide.tools.migrate.Migrate; + +/** The migration tool, see `usage` **/ +class Cli { + + static function usage() { + Sys.println("usage: hl migrate.hl [--out | --in-place] [--report ] [--verbose] +Upgrades the FX data of the prefab files of a project (" + Migrate.EXTENSIONS.join(", ") + ") to version " + hrt.prefab.fx.FX.VERSION + ". +With no option, a dry run: lists the files that would change, the changes and the warnings. + --out write the migrated files to , under their path in the res dir + --in-place rewrite the files of the project + --report write the full report (markdown) to + --verbose list every change of every file"); + } + + static function main() { + // the heaps watchdog would stop a project that takes more than 30s + hxd.System.allowTimeout = false; + var args = Sys.args(); + var res : String = null, out : String = null, inPlace = false, reportFile : String = null, verbose = false; + var i = 0; + while( i < args.length ) { + var a = args[i++]; + switch( a ) { + case "--out": out = args[i++]; + case "--in-place": inPlace = true; + case "--report": reportFile = args[i++]; + case "--verbose": verbose = true; + case "--help", "-h": usage(); return; + default: + if( StringTools.startsWith(a, "-") || res != null ) { + usage(); + Sys.exit(1); + } + res = a; + } + } + if( res == null || (out != null && inPlace) ) { + usage(); + Sys.exit(1); + } + var t0 = haxe.Timer.stamp(); + var result = Migrate.migrateProject(res); + var changed = [for( f in result.files ) if( f.changed ) f]; + var md = report(res, result, verbose); + if( reportFile != null ) + sys.io.File.saveContent(reportFile, md); + Sys.println(md.split("\n## ")[0]); + if( out != null || inPlace ) { + var dir = inPlace ? res : out; + for( f in changed ) { + var path = dir + "/" + f.path; + sys.FileSystem.createDirectory(haxe.io.Path.directory(path)); + sys.io.File.saveContent(path, Migrate.toText(f.data)); + } + Sys.println('${changed.length} files written to $dir'); + } else + Sys.println("dry run, nothing written (--out or --in-place to write)"); + var parents = 0, warnings = 0, errors = result.errors.length; + for( f in result.files ) { + parents += f.report.parents.length; + warnings += f.report.warnings.length; + errors += f.report.errors.length; + } + Sys.println('${result.files.length} files read, ${changed.length} changed, $parents objects moved under a parent, $warnings warnings, $errors errors, ${Math.round((haxe.Timer.stamp() - t0) * 10) / 10}s'); + Sys.exit(errors > 0 ? 1 : 0); + } + + public static function report( res : String, result : ProjectResult, verbose : Bool ) : String { + var b = new StringBuf(); + inline function line( s : String ) b.add(s + "\n"); + var changed = [for( f in result.files ) if( f.changed ) f]; + var kinds = new Map(); + for( f in result.files ) + for( c in f.report.changes ) { + var k = ~/ [^ ]+ ->| of [^ ]+/g.replace(c.kind, ""); + kinds.set(k, (kinds.get(k) ?? 0) + 1); + } + line('# FX data version ${hrt.prefab.fx.FX.VERSION} migration of $res\n'); + line('${result.files.length} files, ${changed.length} to change.\n'); + var keys = [for( k in kinds.keys() ) k]; + keys.sort(Reflect.compare); + if( keys.length > 0 ) { + line("| Change | Count |"); + line("|---|--:|"); + for( k in keys ) line('| $k | ${kinds.get(k)} |'); + } + var parents = [for( f in result.files ) for( p in f.report.parents ) '${f.path} ${p.path} (content now ${p.path}.${p.child}): ${p.reason}']; + if( parents.length > 0 ) { + line("\n## Objects moved under a parent\n"); + line("The parent takes the name, place, position and animation of the object; the object keeps its base rotation and scale and its content under the new name.\n"); + for( p in parents ) line('- $p'); + } + var warnings = [for( f in result.files ) for( w in f.report.warnings ) w]; + if( warnings.length > 0 ) { + line("\n## Warnings\n"); + for( w in warnings ) line('- $w'); + } + var errors = result.errors.concat([for( f in result.files ) for( e in f.report.errors ) e]); + if( errors.length > 0 ) { + line("\n## Errors\n"); + line("What could not be upgraded, left as it is.\n"); + for( e in errors ) line('- $e'); + } + line("\n## Files\n"); + for( f in changed ) { + line('- ${f.path} (${f.report.changes.length} changes)'); + if( verbose ) + for( c in f.report.changes ) line(' - ${c.path}: ${c.kind}'); + } + return b.toString(); + } +} + +#end diff --git a/hide/tools/migrate/Migrate.hx b/hide/tools/migrate/Migrate.hx new file mode 100644 index 000000000..a517171a2 --- /dev/null +++ b/hide/tools/migrate/Migrate.hx @@ -0,0 +1,262 @@ +package hide.tools.migrate; + +#if fx_migration + +import hrt.prefab.fx.FX; + +typedef Change = { path : String, kind : String }; + +typedef FileReport = { + var file : String; + /** By path in the file **/ + var changes : Array; + /** Old data approximated or left as it is **/ + var warnings : Array; + /** Old data that cannot be upgraded, left as it is **/ + var errors : Array; + /** `path`: the parent, at the original path of the object; `child`: the new name of the object **/ + var parents : Array<{ path : String, child : String, reason : String }>; +} + +/** The files of a project, for the overrides of the references of a file **/ +interface Context { + /** The data of the file `path` as it is stored, a fresh copy, null if there is none **/ + function readRaw( path : String ) : Dynamic; + /** The data of the file `path` migrated, a fresh copy, null if there is none **/ + function migrated( path : String ) : Dynamic; + /** Migrates `data` as if it was the file `path` **/ + function upgradeAs( data : Dynamic, path : String ) : Dynamic; +} + +typedef FileResult = { + var path : String; + /** Migrated in place **/ + var data : Dynamic; + var changed : Bool; + var report : FileReport; +} + +typedef ProjectResult = { + var files : Array; + var errors : Array; +} + +/** + Upgrades FX data saved in an older version (`hrt.prefab.fx.FX.VERSION`, on the root of the data: + an FX, or an emitter out of any FX) to the current one (`STEPS`). Compiled with `-D fx-migration` only. + + - On a whole project, once: `migrateProject`, or the tool (`Cli`, hide/migrate.hxml). The project is + migrated in memory, the files that change are written by `toText` (the prefab save of hide). The + overrides of the references to the migrated files are migrated with them. The upgrade reads the + prefab classes (the defaults of their fields, what their curves animate): the tool of a game with + its own prefabs is built with them, the prefab types the build does not have are reported as errors. + - On load, as a fallback (`FX.checkVersion`): FX data is then upgraded alone, in memory, once per loaded resource. The + overrides of its references are kept: they are applied on the files they override, still in the + old version, and the result is upgraded when it loads. +**/ +@:keep +class Migrate { + + /** The upgrade of each older version to the current one. Version 1, the format before the curves rework, was never released: it has none. **/ + public static var STEPS : Map Void> = [ + 0 => (data, root, report) -> new Upgrade0(null, report).run(data, root), + ]; + + /** Loaded as prefabs (`hxd.res.Config`) **/ + public static var EXTENSIONS = ["prefab", "fx", "fx2d", "l3d"]; + + public static function newReport( file : String ) : FileReport { + return { file: file, changes: [], warnings: [], errors: [], parents: [] }; + } + + public static function versionOf( data : Dynamic ) : Int { + var v : Dynamic = Reflect.field(data, "version"); + return v == null ? 0 : Std.int(v); + } + + public static function canUpgrade( version : Int ) { + return STEPS.exists(version); + } + + /** Upgrades in place the root of FX data from `version`, `root` when it is the root of its file. **/ + public static function upgradeTree( data : Dynamic, version : Int, root : Bool, report : FileReport ) { + if( !canUpgrade(version) ) + throw 'FX data of version $version cannot be upgraded to version ${FX.VERSION}'; + STEPS.get(version)(data, root, report); + // the FX inside are upgraded with it + function mark( d : Dynamic ) { + if( isFX(d) ) RawNode.setField(d, "version", FX.VERSION); + var children : Array = Reflect.field(d, "children"); + if( children != null ) for( c in children ) mark(c); + } + mark(data); + RawNode.setField(data, "version", FX.VERSION); + } + + static function isA( d : Dynamic, cl : Class ) { + var c = RawNode.classOf(Reflect.field(d, "type")); + return c != null && hrt.prefab.Prefab.isOfType(c, cl); + } + + static function isFX( d : Dynamic ) return isA(d, hrt.prefab.fx.FX) || isA(d, hrt.prefab.fx.FX2D); + static function isEmitter( d : Dynamic ) return isA(d, hrt.prefab.fx.Emitter) || isA(d, hrt.prefab.fx.Emitter2D); + + /** Migrates in place the data of the file `path`: its FX data, and the overrides of its references (`ctx` reads the files they override). **/ + public static function migrateData( data : Dynamic, path : String, ctx : Context ) : FileResult { + var report = newReport(path); + var changed = false; + function walk( d : Dynamic ) { + if( d == null || !Reflect.isObject(d) ) return; + if( isFX(d) || isEmitter(d) ) { + var v = versionOf(d); + if( v != FX.VERSION ) { + upgradeTree(d, v, d == data, report); + changed = true; + } + return; + } + var children : Array = Reflect.field(d, "children"); + if( children != null ) for( c in children ) walk(c); + } + walk(data); + if( Upgrade0.upgradeReferences(data, ctx, report) ) + changed = true; + return { path: path, data: data, changed: changed, report: report }; + } + + /** The file of migrated data, as hide saves it **/ + public static function toText( data : Dynamic ) : String { + var p = @:privateAccess hrt.prefab.Prefab.createFromDynamic(hrt.prefab.Diff.deepCopy(data), null, new hrt.prefab.ContextShared(null, false)); + return haxe.Json.stringify(@:privateAccess p.serialize(), null, "\t"); + } + + /** Migrates every prefab file under `resDir` in memory: nothing is written. **/ + public static function migrateProject( resDir : String, ?extensions : Array ) : ProjectResult { + return run(new ProjectContext(resDir, extensions ?? EXTENSIONS)); + } + + /** Migrates a project given as its files (resource path => text), in memory **/ + static function migrateTexts( files : Map ) : ProjectResult { + return run(new ProjectContext(null, null, files)); + } + + static function run( ctx : ProjectContext ) : ProjectResult { + var out : ProjectResult = { files: [], errors: [] }; + for( path in ctx.paths ) { + var r = try ctx.result(path) catch( e : Dynamic ) { + out.errors.push('$path: $e'); + null; + } + if( r != null ) + out.files.push(r); + } + for( e in ctx.errors ) + if( out.errors.indexOf(e) < 0 ) + out.errors.push(e); + return out; + } + + public static dynamic function log( msg : String ) { + #if sys + Sys.println(msg); + #else + trace(msg); + #end + } + + static var warned = false; + + /** The upgrade on load of `FX.checkVersion` **/ + public static function fallback( data : Dynamic, version : Int, root : Bool ) { + var report = newReport(Reflect.field(data, "name")); + upgradeTree(data, version, root, report); + if( !warned ) { + warned = true; + log('[hide] FX data of version $version is upgraded to version ${FX.VERSION} on load. Migrate the project once with the migration tool (hide/migrate.hxml).'); + } + for( w in report.warnings ) + log('[hide] warning: $w'); + for( e in report.errors ) + log('[hide] error: $e'); + } +} + +private class ProjectContext implements Context { + var dir : String; + public var paths : Array = []; + public var errors : Array = []; + var texts = new Map(); + var results = new Map(); + var inProgress = new Map(); + + public function new( dir : String, extensions : Array, ?files : Map ) { + if( files != null ) { + for( path => text in files ) { + texts.set(path, text); + paths.push(path); + } + paths.sort(Reflect.compare); + return; + } + this.dir = haxe.io.Path.removeTrailingSlashes(StringTools.replace(dir, "\\", "/")); + function scan( rel : String ) { + var full = rel == "" ? this.dir : this.dir + "/" + rel; + var entries = sys.FileSystem.readDirectory(full); + entries.sort(Reflect.compare); + for( f in entries ) { + var r = rel == "" ? f : rel + "/" + f; + if( sys.FileSystem.isDirectory(full + "/" + f) ) { + if( f.charAt(0) != "." ) scan(r); + } else if( extensions.indexOf(haxe.io.Path.extension(f).toLowerCase()) >= 0 ) + paths.push(r); + } + } + scan(""); + } + + function text( path : String ) : String { + if( texts.exists(path) || dir == null ) return texts.get(path); + var full = dir + "/" + path; + var t = sys.FileSystem.exists(full) ? sys.io.File.getContent(full) : null; + texts.set(path, t); + return t; + } + + public function readRaw( path : String ) : Dynamic { + var t = text(path); + return t == null ? null : try haxe.Json.parse(t) catch( e : Dynamic ) null; + } + + public function result( path : String ) : FileResult { + var r = results.get(path); + if( r != null ) return r; + var data = readRaw(path); + if( data == null ) return null; + if( inProgress.exists(path) ) { + errors.push('$path: references itself through other files, its overrides are kept as they are'); + return null; + } + inProgress.set(path, true); + r = try Migrate.migrateData(data, path, this) catch( e : Dynamic ) { + inProgress.remove(path); + throw e; + } + inProgress.remove(path); + results.set(path, r); + return r; + } + + public function migrated( path : String ) : Dynamic { + var r = try result(path) catch( e : Dynamic ) { + errors.push('$path: $e'); + null; + } + return r == null ? null : hrt.prefab.Diff.deepCopy(r.data); + } + + public function upgradeAs( data : Dynamic, path : String ) : Dynamic { + return Migrate.migrateData(data, path, this).data; + } +} + +#end diff --git a/hide/tools/migrate/RawNode.hx b/hide/tools/migrate/RawNode.hx new file mode 100644 index 000000000..7fd5113b1 --- /dev/null +++ b/hide/tools/migrate/RawNode.hx @@ -0,0 +1,142 @@ +package hide.tools.migrate; + +#if fx_migration + +/** + A prefab of the raw (json) data of a file, before it is loaded. The upgrade steps work on these: + they change `data` in place, and keep the tree in sync when they add, move or rename nodes. +**/ +class RawNode { + public var data : Dynamic; + public var parent : RawNode; + public var children : Array = []; + /** The name the prefab gets once loaded (curves: `.` becomes `:`) **/ + public var name : String; + public var type : String; + public var cls : Class; + public var isCurve : Bool; + /** The child indices of the node from the root, in the data as it was read. Null for a node made by the upgrade. **/ + public var origin : Array; + + public function new() {} + + public static function build( data : Dynamic, parent : RawNode, ?origin : Array ) : RawNode { + var n = new RawNode(); + n.data = data; + n.parent = parent; + n.origin = origin; + n.type = get(data, "type"); + n.cls = classOf(n.type); + n.isCurve = n.cls != null && hrt.prefab.Prefab.isOfType(n.cls, hrt.prefab.Curve); + n.name = loadedName(n); + var children : Array = get(data, "children"); + if( children != null ) + for( c in children ) + if( c != null ) + n.children.push(build(c, n, origin == null ? null : origin.concat([n.children.length]))); + return n; + } + + /** Mirrors `Curve.load` (game build): a `.` in the name of a curve becomes `:`. **/ + static function loadedName( n : RawNode ) : String { + var name : String = get(n.data, "name") ?? ""; + return n.isCurve ? StringTools.replace(name, ".", ":") : name; + } + + public function rename( name : String ) { + data.name = name; + this.name = isCurve ? StringTools.replace(name, ".", ":") : name; + } + + public static function classOf( type : String ) : Class { + if( type == null ) return null; + var info = @:privateAccess hrt.prefab.Prefab.getPrefabInfoByName(type); + return info?.prefabClass; + } + + /** A field of raw data **/ + public static inline function get( d : Dynamic, f : String ) : Dynamic { + return Reflect.field(d, f); + } + + public function isA( cl : Class ) { + return cls != null && hrt.prefab.Prefab.isOfType(cls, cl); + } + + public function enabled() { + return get(data, "enabled") != false; + } + + /** Like `Prefab.getUniqueName`: `name-N` for the Nth sibling of the same name. **/ + public function uniqueName() { + if( parent == null ) + return ""; + var n = 0; + for( c in parent.children ) { + if( c == this ) break; + if( c.name == name ) n++; + } + return n > 0 ? name + "-" + n : name; + } + + /** The unique path from the root, for messages **/ + public function path() { + var parts = []; + var p = this; + while( p != null && p.parent != null ) { + parts.unshift(p.uniqueName()); + p = p.parent; + } + return parts.join("."); + } + + public function curveChildren() { + return [for( c in children ) if( c.isCurve ) c]; + } + + /** Appends a child (data and node) **/ + public function addChild( d : Dynamic ) : RawNode { + var arr : Array = get(data, "children"); + if( arr == null ) { + arr = []; + setField(data, "children", arr); + } + arr.push(d); + var n = build(d, this); + children.push(n); + return n; + } + + /** Rebuilds the nodes of the children from the data (the data of the node was replaced) **/ + public function rebuildChildren() { + var rebuilt = build(data, parent); + children = rebuilt.children; + for( c in children ) c.parent = this; + } + + /** + Sets a field of an object, before `children` when it is a new one: the order a prefab saves + its fields in (`Prefab.serialize` adds the children last). + **/ + public static function setField( o : Dynamic, f : String, v : Dynamic ) { + if( Reflect.hasField(o, f) || !Reflect.hasField(o, "children") ) { + Reflect.setField(o, f, v); + return; + } + var fields = Reflect.fields(o); + var values = [for( k in fields ) Reflect.field(o, k)]; + for( k in fields ) Reflect.deleteField(o, k); + for( i => k in fields ) { + if( k == "children" ) Reflect.setField(o, f, v); + Reflect.setField(o, k, values[i]); + } + } + + /** Replaces the fields of `o` by the ones of `by`, in their order (`o` keeps its identity) **/ + public static function replaceFields( o : Dynamic, by : Dynamic ) { + for( k in Reflect.fields(o) ) Reflect.deleteField(o, k); + for( k in Reflect.fields(by) ) Reflect.setField(o, k, Reflect.field(by, k)); + } +} + +#end diff --git a/hide/tools/migrate/TransformMath.hx b/hide/tools/migrate/TransformMath.hx new file mode 100644 index 000000000..d0ed64236 --- /dev/null +++ b/hide/tools/migrate/TransformMath.hx @@ -0,0 +1,176 @@ +package hide.tools.migrate; + +#if fx_migration + +/** + Matrix helpers of the transform re-bakes of `Upgrade0`: the exact forms of an old animated transform, + checked at fixed values of the curves, and the decomposition of the sampled ones. `classes` tells, per component, which curve animates it: the + components of the same class have the same curve (or curves with the same data), -1 when none does. +**/ +class TransformMath { + + /** Values of the curves the forms are checked at, by class **/ + static var SCALES = [[1.7, 0.6, 2.3], [0.4, 2.9, 1.3], [-0.8, 1.5, 0.3]]; + static var ANGLES = [[21.2, -74.5, 166.1], [-133.7, 38.9, 97.3], [251.3, 12.4, -58.6]]; + static var PERMS = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]; + + static function rotation( r : Array ) { + var m = new h3d.Matrix(); + m.initRotation(hxd.Math.degToRad(r[0]), hxd.Math.degToRad(r[1]), hxd.Math.degToRad(r[2])); + return m; + } + + static function same( a : h3d.Matrix, b : h3d.Matrix ) { + for( e in [a._11 - b._11, a._12 - b._12, a._13 - b._13, a._21 - b._21, a._22 - b._22, a._23 - b._23, a._31 - b._31, a._32 - b._32, a._33 - b._33] ) + if( Math.abs(e) > 1e-9 ) return false; + return true; + } + + static function values( classes : Array, sample : Array, def : Float ) { + return [for( c in classes ) c < 0 ? def : sample[c]]; + } + + /** + The permutation `p` of the scale components such that base rotation * scale(v) = scale(v[p]) * base + rotation, for any value `v` of the curves. Null when there is none: the base rotation shears the + animated scale. + **/ + public static function scalePermutation( rot : Array, classes : Array ) : Array { + var rb = rotation(rot); + var a = new h3d.Matrix(), b = new h3d.Matrix(); + for( p in PERMS ) { + var ok = true; + for( s in SCALES ) { + var v = values(classes, s, 1); + a.initScale(v[0], v[1], v[2]); + a.multiply(rb, a); + b.initScale(v[p[0]], v[p[1]], v[p[2]]); + b.multiply(b, rb); + if( !same(a, b) ) { + ok = false; + break; + } + } + if( ok ) return p; + } + return null; + } + + /** The smallest multiple of the loop periods, 0 when there is none (or not under 16 times the longest one) **/ + public static function commonPeriod( periods : Array ) : Float { + var longest = 0.; + for( p in periods ) if( p > longest ) longest = p; + if( longest <= 0 ) return 0; + for( k in 1...17 ) { + var c = longest * k; + if( Lambda.foreach(periods, p -> p > 0 && Math.abs(c / p - Math.round(c / p)) < 1e-6) ) + return c; + } + return 0; + } + + public static function scaleMatrix( s : Array ) { + var m = new h3d.Matrix(); + m.initScale(s[0], s[1], s[2]); + return m; + } + + /** + Scale and euler angles (radians, closest to `prev`) of a matrix (its 3x3 part), `signs` telling + the sign of each scale. `error`: what the rebuilt matrix misses of it. + **/ + public static function decompose( m : h3d.Matrix, prev : Array, signs : Array ) : { s : Array, r : Array, error : Float, m : h3d.Matrix } { + var rows = [[m._11, m._12, m._13], [m._21, m._22, m._23], [m._31, m._32, m._33]]; + var det = m._11 * (m._22 * m._33 - m._23 * m._32) - m._12 * (m._21 * m._33 - m._23 * m._31) + m._13 * (m._21 * m._32 - m._22 * m._31); + var sg = [for( v in signs ) v < 0 ? -1. : 1.]; + if( (sg[0] * sg[1] * sg[2] < 0) != (det < 0) ) + sg = [-sg[0], -sg[1], -sg[2]]; + var s = [for( i in 0...3 ) sg[i] * Math.sqrt(rows[i][0] * rows[i][0] + rows[i][1] * rows[i][1] + rows[i][2] * rows[i][2])]; + var r = [for( i in 0...3 ) [for( j in 0...3 ) s[i] == 0 ? (i == j ? 1. : 0.) : rows[i][j] / s[i]]]; + inline function closest( a : Float, ref : Float ) { + var tau = Math.PI * 2; + return a + Math.round((ref - a) / tau) * tau; + } + var cands = []; + var cy = Math.sqrt(r[0][0] * r[0][0] + r[0][1] * r[0][1]); + if( cy > 1e-6 ) { + var y = Math.atan2(-r[0][2], cy); + cands.push([Math.atan2(r[1][2], r[2][2]), y, Math.atan2(r[0][1], r[0][0])]); + cands.push([Math.atan2(-r[1][2], -r[2][2]), Math.PI - y, Math.atan2(-r[0][1], -r[0][0])]); + } else { + var z = prev[2]; + if( -r[0][2] > 0 ) // y = 90 + cands.push([z + Math.atan2(r[1][0], r[1][1]), Math.PI / 2, z]); + else + cands.push([Math.atan2(-r[1][0], r[1][1]) - z, -Math.PI / 2, z]); + } + var best = null, bestD = Math.POSITIVE_INFINITY; + for( c in cands ) { + var a = [for( i in 0...3 ) closest(c[i], prev[i])]; + var dist = 0.; + for( i in 0...3 ) dist += (a[i] - prev[i]) * (a[i] - prev[i]); + if( dist < bestD ) { + bestD = dist; + best = a; + } + } + // error of the rebuilt matrix + var check = new h3d.Matrix(); + check.initScale(s[0], s[1], s[2]); + check.rotate(best[0], best[1], best[2]); + var err = 0.; + var scaleRef = Math.max(1e-6, Math.max(Math.abs(s[0]), Math.max(Math.abs(s[1]), Math.abs(s[2])))); + for( e in [check._11 - m._11, check._12 - m._12, check._13 - m._13, check._21 - m._21, check._22 - m._22, check._23 - m._23, check._31 - m._31, check._32 - m._32, check._33 - m._33] ) + err = Math.max(err, Math.abs(e) / scaleRef); + return { s: s, r: best, error: err, m: check }; + } + + /** The euler triples of the same rotation as `rot`: itself, the other branch, quarter turns, the gimbal family **/ + static function triples( rot : Array ) : Array> { + var ref = rotation(rot); + var cands = [rot, [rot[0] + 180, 180 - rot[1], rot[2] + 180]]; + for( i in 0...4 ) for( j in 0...4 ) for( k in 0...4 ) + cands.push([i * 90. - 180, j * 90. - 180, k * 90. - 180]); + for( i in -4...5 ) { + cands.push([rot[0] + i * 90, rot[1], rot[2] + i * 90]); + cands.push([rot[0] + i * 90, rot[1], rot[2] - i * 90]); + } + var out = []; + inline function equal( a : Array, b : Array ) { + return Lambda.foreach([0, 1, 2], i -> Math.abs(Math.cos(hxd.Math.degToRad(a[i] - b[i])) - 1) < 1e-12); + } + for( c in cands ) + if( same(rotation(c), ref) && !Lambda.exists(out, o -> equal(o, c)) ) + out.push(c); + return out; + } + + /** + Base rotation * rotation(r) as euler angles affine in the curves: angle `j` is `base[j] + sign[j] * r[perm[j]]` + (`base[j]` alone when no curve animates component `perm[j]`), `base` an euler triple of the base rotation, + for any value `r` of the curves (degrees). Null when there is none. + **/ + public static function rotationForm( rot : Array, classes : Array ) : { base : Array, perm : Array, sign : Array } { + var rb = rotation(rot); + var a = new h3d.Matrix(); + var signs = [for( k in 0...8 ) [k & 1 == 0 ? 1. : -1., k & 2 == 0 ? 1. : -1., k & 4 == 0 ? 1. : -1.]]; + for( base in triples(rot) ) + for( p in PERMS ) + for( sg in signs ) { + if( Lambda.exists([0, 1, 2], j -> classes[p[j]] < 0 && sg[j] < 0) ) continue; + var ok = true; + for( s in ANGLES ) { + var r = values(classes, s, 0); + a.multiply(rb, rotation(r)); + if( !same(a, rotation([for( j in 0...3 ) base[j] + sg[j] * r[p[j]]])) ) { + ok = false; + break; + } + } + if( ok ) return { base: base, perm: p, sign: sg }; + } + return null; + } +} + +#end diff --git a/hide/tools/migrate/Upgrade0.hx b/hide/tools/migrate/Upgrade0.hx new file mode 100644 index 000000000..5540aa6e3 --- /dev/null +++ b/hide/tools/migrate/Upgrade0.hx @@ -0,0 +1,2385 @@ +package hide.tools.migrate; + +#if fx_migration + +import hrt.prefab.fx.Value; +import hrt.prefab.fx.Evaluator; +import hide.tools.migrate.Migrate.FileReport; +import hide.tools.migrate.RawNode.get; +import hrt.prefab.Diff.deepCopy; + +/** A component of a vector property after the upgrade: `a * old[src] + b`, the value of the prefab when `src` is null **/ +private typedef ChannelMap = { src : Null, a : Float, b : Float }; + +/** The curves of a vector property: `uniform` when one curve is all of them **/ +private typedef Channels = { uniform : RawNode, comps : Array }; + +/** A sampled re-bake of the rotation (and scale) of an object, `problem` tells why it cannot stand for the old transform **/ +private typedef Resample = { + var cs : Channels; + var cr : Channels; + var scale : Array; + var rot : Array; + var withScale : Bool; + var loop : Bool; + /** What the keys are over: "" for the FX time, else an FX parameter **/ + var input : String; + var scaleKeys : Array>; + var rotKeys : Array>; + var problem : String; + /** Why the keys only stand for the old transform at the default of the parameters **/ + var approx : String; + /** How far the old runtime was from a rigid transform **/ + var nonRigid : Float; +} + +/** + The mode of a param component (format 1), a curve by its path from the emitter. The bounds of MCurve and MCurveRange are baked + into their curves when saved, the middle of the ones of MRandomCurve too (its curve is then absolute, its bounds -amount and +amount around it). The curve of MCurveRange is an old random blend: its two child curves become its two datas. +**/ +private enum Mode { + MConstant( v : Float ); + MRange( min : Float, max : Float ); + MCurve( curve : String, from : Float, to : Float ); + MRandomCurve( curve : String, fromMin : Float, fromMax : Float, toMin : Float, toMax : Float ); + MCurveRange( curve : String, aFrom : Float, aTo : Float, bFrom : Float, bTo : Float ); +} + +private enum abstract Group(Int) { + var Spawn; + var Particle; + var OverTime; +} + +/** A param with modes of format 1, and the old field it comes from **/ +private typedef ParamDef = { name : String, old : String, group : Group, def : Array }; + +/** + Upgrade of the prefab format 0 to 1, on the raw data of one file: + + - Emitter params with modes (`Emitter`, `Emitter2D`, the particle shaders), from the base value, + random amount and curves by name of the old params. The curves of the Curve and Curve Range modes + hold their values, their bounds baked into them. + - Animation curves hold the value they animate: the constants the old FX composed them with are + baked into them (exactly when the old value is linear in the curve), and the ones of the emitter + are named after what they animate (`scaleMultiplier`). The curves of the Spawn and Emission params + are their Curve and Curve Range modes over the emitter time (`startSpeed:x`, `emitRate`), the + particle params prefixed with `part` (`partScale`). + - Orbit speeds go from radians to degrees per second. + - Transforms are re-baked exactly, the curves of the rotation and scale affine in the old ones: the + base rotation folded into the euler angles (from another euler triple of the same rotation, which + the prefab then stores, when it takes one), the animated scale moved before the base rotation + (its components permuted). An object whose rotation the euler curves cannot follow gets a parent + object, which takes its name, its place, its position and its animation as it is; it keeps its + base rotation and scale and its content, under a new name (`_base`). The paths of the file + through it are updated, the ones that read the object get the child. A scale the base rotation + shears, and an object that cannot get a parent (`noParent`), are sampled to what the old runtime + showed: `setTransform` kept a scale and a quaternion of the animated matrix (a shear is lost). The + samples are over the input of the curves: the FX time, or the FX parameter that remaps them. + - The curves are FX curves (`hrt.prefab.fx.Curve`, type `fxcurve`). + - A reference to a curve has no scale or offset: a scaled reference, of the data or made by the + upgrade, becomes a copy of its curve with the scale and offset applied to its keys. + - The overrides of the references are rewritten for the upgraded files they override (see + `upgradeReference`), reading the other files of the project through the context. + + It writes the data of format 1. The sampled re-bakes evaluate the old curves with the runtime + (`Curve.makeVal`, `Evaluator`): they rely on it reading the old curves as the old runtime did. +**/ +@:access(hide.tools.migrate.Migrate) +class Upgrade0 { + + /** Rotation tolerance of the sampled re-bakes, in degrees **/ + static inline var ROTATION_TOLERANCE = 0.02; + /** Scale tolerance of the sampled re-bakes, relative **/ + static inline var SCALE_TOLERANCE = 1e-4; + /** Error of a sampled re-bake beyond which the object gets a parent, relative to the scale of the matrix rows **/ + static inline var MATRIX_TOLERANCE = 5e-4; + /** Sampling step of the re-bakes, in seconds **/ + static inline var SAMPLE_STEP = 1.0 / 240.0; + /** Suffix of the name of an object moved under a parent that takes its name **/ + static inline var BASE_SUFFIX = "_base"; + + static function num( v : Dynamic, def : Float ) : Float { + return Std.isOfType(v, Float) || Std.isOfType(v, Int) ? (v : Float) : def; + } + + /** An object with its fields in this order (`[name, value, name, value...]`), as it is written **/ + static function obj( fields : Array ) : Dynamic { + var o = {}; + var i = 0; + while( i < fields.length ) { + Reflect.setField(o, fields[i], fields[i + 1]); + i += 2; + } + return o; + } + + static function sameName( a : String, b : String ) return a == b || a.toLowerCase() == b.toLowerCase(); + + /** The value a missing field of a curve key (or handle) loads with, see `CurveKey` **/ + static inline var CurveKeyDefault = -1.0; + + var ctx : Migrate.Context; + var report : FileReport; + var file : String; + var root : RawNode; + var users : Map> = new Map(); + var remapUsers : Map> = new Map(); + var bakedModeCurves : Map = new Map(); + var mergedBlends : Map = new Map(); + /** The curves that hold their new values **/ + var upgraded = new haxe.ds.ObjectMap(); + var parameters : Map = new Map(); + var fxDuration : Float = 5.0; + var pristine : Dynamic; + var mirror : hrt.prefab.Prefab; + var fileRoot : Bool; + + public function new( ctx : Migrate.Context, report : FileReport ) { + this.ctx = ctx; + this.file = report.file; + this.report = report; + } + + /** + Upgrades in place `data`, the root of FX data (an FX, an emitter out of any FX), `fileRoot` when it + is the root of its file (the paths of the curve references start there, they are not written otherwise). + **/ + public function run( data : Dynamic, fileRoot : Bool ) { + pristine = deepCopy(data); + root =RawNode.build(data, null, []); + this.fileRoot = fileRoot; + indexUsers(root); + checkInputs(root); + fxPass(root); + emitterPass(root); + randomBlendPass(root); + scaledReferencePass(root); + curvePass(data); + roundNew(data, pristine); + } + + /** A number computed by the upgrade as it is written: 6 significant digits, 0 under 1e-6 (the float noise of the bakes) **/ + static function roundNumber( v : Float ) : Float { + var a = Math.abs(v); + if( !Math.isFinite(v) || a == Math.ffloor(a) ) + return v; + if( a < 1e-6 ) + return 0.; + var scale = Math.pow(10, 5 - Math.floor(hxd.Math.log10(a))); + return Math.fround(v * scale) / scale; + } + + /** Rounds the numbers of `d` that are not in `source`, its data before the upgrade (`roundNumber`): the ones of the data are kept as they are **/ + static function roundNew( d : Dynamic, source : Dynamic ) { + var known = new Map(); + function isObject( v : Dynamic ) return v != null && Reflect.isObject(v) && !Std.isOfType(v, String) && !Std.isOfType(v, Array); + function collect( v : Dynamic ) { + if( isNumber(v) ) + known.set(Std.string(v), true); + else if( Std.isOfType(v, Array) ) + for( x in (v : Array) ) collect(x); + else if( isObject(v) ) + for( f in Reflect.fields(v) ) collect(Reflect.field(v, f)); + } + collect(source); + function isNew( v : Dynamic ) return isNumber(v) && !known.exists(Std.string(v)); + function round( v : Dynamic ) { + if( Std.isOfType(v, Array) ) { + var a : Array = v; + for( i in 0...a.length ) + if( isNew(a[i]) ) a[i] = roundNumber(a[i]) else round(a[i]); + } else if( isObject(v) ) + for( f in Reflect.fields(v) ) { + var x : Dynamic = Reflect.field(v, f); + if( isNew(x) ) Reflect.setField(v, f, roundNumber(x)) else round(x); + } + } + round(d); + } + + /** `Curve.makeVal` reads no scale or offset on a reference to a curve: the scaled references become a copy of their curve. A broken reference reads 0, it is kept. **/ + function scaledReferencePass( n : RawNode ) { + function isScaled() return n.isCurve && blendMode(n) == 3 && (num(get(n.data, "scale"), 1) != 1 || num(get(n.data, "offset"), 0) != 0); + // the copy of a reference to a reference is a scaled reference again + while( isScaled() && locate(get(n.data, "blendParam")) != null ) { + dereference(n); + change(n, "scaled reference, copy of its curve"); + } + for( c in n.children ) + scaledReferencePass(c); + } + + /** + The curves get the format of `hrt.prefab.fx.Curve`: the blend mode by name, a blend holds the keys of its two child curves + (`datas`), the curve of two datas of a Curve Range is a Range, the parameter of a blend or a time remap is `param`, the + path of a reference `refPath`. + **/ + function curvePass( d : Dynamic ) { + var type = get(d, "type"); + if( type == "curve" || type == "fxcurve" ) { + d.type = "fxcurve"; + upgradeCurve(d); + // the runtime never read the children of a curve but the two of a blend + var children : Array = get(d, "children"); + if( children != null && children.length > 0 ) { + report.warnings.push('$file ${d.name}: ${children.length} curves under a curve, never read, removed'); + Reflect.deleteField(d, "children"); + } + } + var children : Array = get(d, "children"); + if( children != null ) for( c in children ) if( c != null ) curvePass(c); + } + + static function upgradeCurve( d : Dynamic ) { + var b : Dynamic = get(d, "blendMode"); + var mode : Int = b == null ? 0 : Std.isOfType(b, Int) ? b : Std.parseInt(Std.string(b)) ?? 0; + var blendParam : String = get(d, "blendParam"), remap : String = get(d, "remapPath"); + for( f in ["blendMode", "blendParam", "remapPath"] ) + Reflect.deleteField(d, f); + switch( mode ) { + case 1: + var children : Array = get(d, "children") ?? []; + var sub = [for( c in children ) if( c != null && (c.type == "curve" || c.type == "fxcurve") ) c].slice(0, 2); + for( f in ["keys", "keyMode", "loop", "children"] ) + Reflect.deleteField(d, f); + if( sub.length == 2 && blendParam != null ) { + RawNode.setField(d, "blendMode", "Blend"); + RawNode.setField(d, "param", blendParam); + RawNode.setField(d, "datas", [datum(sub[0]), datum(sub[1])]); + } else if( sub.length > 0 ) { + // the runtime read its first child curve + var k = datum(sub[0]); + RawNode.setField(d, "keys", k.keys); + if( k.loop == true ) RawNode.setField(d, "loop", true); + } else + RawNode.setField(d, "keys", [obj(["time", 0., "value", 0., "mode", 2])]); + case 3: + RawNode.setField(d, "blendMode", "Reference"); + RawNode.setField(d, "refPath", blendParam); + default: + if( get(d, "datas") != null ) + RawNode.setField(d, "blendMode", "Range"); + else if( remap != null && StringTools.startsWith(remap, "$param.") ) + RawNode.setField(d, "param", remap.substr("$param.".length)); + } + } + + /** Upgrades the overrides of the references of a file, true if any changed **/ + public static function upgradeReferences( data : Dynamic, ctx : Migrate.Context, report : FileReport ) : Bool { + var u = new Upgrade0(ctx, report); + u.root = RawNode.build(data, null, []); + u.fileRoot = true; + var n = report.changes.length; + u.referencePass(u.root); + return report.changes.length > n; + } + + function warn( node : RawNode, msg : String ) { + report.warnings.push('$file ${node.path()}: $msg'); + } + + function fail( node : RawNode, msg : String ) { + report.errors.push('$file ${node.path()}: $msg'); + } + + function change( node : RawNode, kind : String ) { + report.changes.push({ path: node.path(), kind: kind }); + } + + /** Fresh instances of the prefab classes, by type, for the defaults of their fields **/ + static var defaults = new Map(); + + /** + The field `name` (the exact name first, else in any case) of the prefab of `n` once loaded: its + value in the data, else the default of its class. Null when the class has no such field, or is + not known to this build. + **/ + static function fieldOf( n : RawNode, name : String ) : Dynamic { + if( n.cls == null ) + return null; + var fields = Type.getInstanceFields(n.cls); + var f = fields.indexOf(name) >= 0 ? name : Lambda.find(fields, f -> f.toLowerCase() == name.toLowerCase()); + if( f == null ) + return null; + if( Reflect.hasField(n.data, f) ) + return get(n.data, f); + var inst = defaults.get(n.type); + if( inst == null ) + defaults.set(n.type, inst = Type.createInstance(n.cls, [null, new hrt.prefab.ContextShared(null, false)])); + return Reflect.field(inst, f); + } + + function isLegacy( n : RawNode ) return n.isCurve && !upgraded.exists(n.data); + function anyLegacy( g : Array ) return Lambda.exists(g, c -> isLegacy(c)); + function markUpgraded( n : RawNode ) upgraded.set(n.data, true); + + // ---------------------------------------------------------------------------------------- + // Paths + + /** The child of `n` at `part` of a path, like `Prefab.locatePrefab`: the exact name first, else `name-N` for the Nth of that name **/ + static function findChild( children : Array, part : String ) : RawNode { + var found = Lambda.find(children, c -> c.name == part); + var dash = part.lastIndexOf("-"); + if( found == null && dash > 0 ) { + var name = part.substr(0, dash), nth = Std.parseInt(part.substr(dash + 1)) ?? 0; + found = Lambda.find(children, c -> c.name == name && nth-- == 0); + } + return found; + } + + /** Like `Prefab.locatePrefab` from `base`, the root of the file by default **/ + function locate( path : String, ?base : RawNode ) : RawNode { + if( path == null || (base == null && !fileRoot) ) + return null; + var p = base ?? root; + for( part in path.split(".") ) { + p = findChild(p.children, part); + if( p == null ) + return null; + } + return p; + } + + /** The path of `node` from the root (`locate`), null if it cannot be written as one. **/ + function pathOf( node : RawNode ) : String { + if( !fileRoot ) + return null; + var parts = []; + var p = node; + while( p != root ) { + if( p == null || p.name == "" || p.name.indexOf("-") >= 0 || p.name.indexOf(".") >= 0 ) + return null; + parts.unshift(p.uniqueName()); + p = p.parent; + } + var path = parts.join("."); + return locate(path) == node ? path : null; + } + + /** The path of `node` from `base`, like `EmitterParam.resolveCurve` reads it. **/ + static function relPath( base : RawNode, node : RawNode ) : String { + var parts = []; + var p = node; + while( p != base ) { + parts.unshift(p.uniqueName()); + p = p.parent; + } + return parts.join("."); + } + + // ---------------------------------------------------------------------------------------- + // Curves + + static function blendMode( n : RawNode ) : Int { + var b : Dynamic = get(n.data, "blendMode"); + if( b == null ) return 0; + return Std.isOfType(b, Int) ? b : Std.parseInt(Std.string(b)) ?? 0; + } + + function newCurve( name : String ) : Dynamic { + var d : Dynamic = obj(["type", "fxcurve", "name", name, "keys", []]); + upgraded.set(d, true); + return d; + } + + /** A name no enabled sibling has (the curves are found by name among the enabled ones) **/ + static function freeName( parent : RawNode, name : String ) { + var n = name, i = 1; + while( Lambda.exists(parent.children, c -> c.enabled() && sameName(c.name, n)) ) + n = name + "_" + (i++); + return n; + } + + /** The curves the runtime does not read (`Curve.makeVal`): a remap by another curve, a remapped blend or reference, a blend of curves that are not plain **/ + function checkInputs( n : RawNode ) { + if( n.isCurve ) { + var remap : String = get(n.data, "remapPath"), b = blendMode(n); + inline function plain( c : RawNode ) return blendMode(c) == 0 && (get(c.data, "remapPath") ?? "") == ""; + if( remap != null && remap != "" && !StringTools.startsWith(remap, "$param.") ) + fail(n, "time remap by another curve, not supported"); + else if( remap != null && remap != "" && (b == 1 || b == 3) ) + fail(n, "time remap of a blend or a reference, not supported"); + if( b == 1 && !Lambda.foreach(n.curveChildren().slice(0, 2), c -> plain(c)) ) + fail(n, "blend of curves that are not plain, not supported"); + } + for( c in n.children ) + checkInputs(c); + } + + function indexUsers( n : RawNode ) { + if( n.isCurve ) { + if( blendMode(n) == 3 ) { + var t = locate(get(n.data, "blendParam")); + if( t != null ) { + var a = users.get(t); + if( a == null ) users.set(t, a = []); + a.push(n); + } + } + var remap : String = get(n.data, "remapPath"); + if( remap != null && !StringTools.startsWith(remap, "$param.") ) { + var t = locate(remap); + if( t != null ) { + var a = remapUsers.get(t); + if( a == null ) remapUsers.set(t, a = []); + a.push(n); + } + } + } + for( c in n.children ) + indexUsers(c); + } + + /** Copies the data of the curve a reference curve uses into it, which stops depending on it. **/ + function dereference( user : RawNode ) { + var target = locate(get(user.data, "blendParam")); + if( target == null ) return; + var s = num(get(user.data, "scale"), 1), o = num(get(user.data, "offset"), 0); + var copy : Dynamic = deepCopy(target.data); + // keep what makes the user what it is + copy.name = user.data.name; + copy.enabled = get(user.data, "enabled"); + if( get(user.data, "remapPath") != null ) { + if( get(copy, "remapPath") != null ) + warn(user, "reference to a remapped curve with its own remap, the remap of the reference is kept"); + copy.remapPath = get(user.data, "remapPath"); + } + if( copy.enabled == null ) Reflect.deleteField(copy, "enabled"); + RawNode.replaceFields(user.data, copy); + // it holds the old values of the curve it used + upgraded.remove(user.data); + users.get(target)?.remove(user); + user.rebuildChildren(); + // the copy may itself be a reference + indexUsers(user); + bakeLinear(user, s, o, false); + } + + /** + Changes the values of a curve to `a * value + b`. The curves that reference it are compensated + to keep reading the same values, unless `compensate` is false. + **/ + function bakeLinear( n : RawNode, a : Float, b : Float, compensate = true ) { + if( a == 1 && b == 0 ) return; + var us = users.get(n); + if( compensate && us != null ) + for( u in us.copy() ) { + if( a == 0 ) { + dereference(u); + continue; + } + var s = num(get(u.data, "scale"), 1), o = num(get(u.data, "offset"), 0); + RawNode.setField(u.data, "scale", s / a); + RawNode.setField(u.data, "offset", o - b * s / a); + } + if( compensate && remapUsers.exists(n) ) + warn(n, "curve used as the time remap of other curves, they read the upgraded values"); + var d = n.data; + switch( blendMode(n) ) { + case 3: + RawNode.setField(d, "scale", num(get(d, "scale"), 1) * a); + RawNode.setField(d, "offset", num(get(d, "offset"), 0) * a + b); + case 1: + // `Curve.makeVal` blends its first two children + var cs = [for( i in 0...hxd.Math.imin(2, n.children.length) ) if( n.children[i].isCurve ) n.children[i]]; + if( cs.length == 0 ) warn(n, "blend curve without child curve"); + for( c in cs ) + bakeLinear(c, a, b, compensate); + default: + var keys : Array = get(d, "keys"); + if( keys == null || keys.length == 0 ) { + // the default keys of `Curve.load` + var mode = get(d, "keyMode") ?? 2; + keys = [obj(["time", 0., "value", 0., "mode", mode]), obj(["time", 1., "value", 1., "mode", mode])]; + RawNode.setField(d, "keys", keys); + } + for( k in keys ) { + k.value = a * num(get(k, "value"), CurveKeyDefault) + b; + for( h in [get(k, "prevHandle"), get(k, "nextHandle")] ) + if( h != null ) + h.dv = a * num(get(h, "dv"), CurveKeyDefault); + } + } + } + + /** A curve that is `a * curve + b`: a reference to it when possible (made a copy by `scaledReferencePass`), else a baked copy. **/ + function linearCopy( src : RawNode, parent : RawNode, name : String, a : Float, b : Float ) : RawNode { + var path = pathOf(src); + if( path != null ) { + var d = newCurve(freeName(parent, name)); + d.blendMode = 3; + d.blendParam = path; + if( a != 1 ) d.scale = a; + if( b != 0 ) d.offset = b; + var n = parent.addChild(d); + var us = users.get(src); + if( us == null ) users.set(src, us = []); + us.push(n); + return n; + } + var d : Dynamic = deepCopy(src.data); + d.name = freeName(parent, name); + var n = parent.addChild(d); + indexUsers(n); + bakeLinear(n, a, b, false); + upgraded.set(d, true); + return n; + } + + /** Marks the old curves as upgraded. The curves of the emitters are left to them, but the root. **/ + function markAll( n : RawNode, base : RawNode, skipEmitters : Bool ) { + if( n.parent != null && n.isCurve && Migrate.isEmitter(n.parent.data) && (skipEmitters || n.parent != base) ) + return; + if( n.isCurve ) + markUpgraded(n); + for( c in n.children ) + markAll(c, base, skipEmitters); + } + + // ---------------------------------------------------------------------------------------- + // Old evaluation (sampled re-bakes) + + /** The prefab of `n` in the file as it was read, loaded as plain prefabs with its curves, to evaluate them. **/ + function mirrorOf( n : RawNode ) : hrt.prefab.Prefab { + if( n.origin == null ) + return null; + if( mirror == null ) { + function strip( d : Dynamic ) : Dynamic { + if( get(d, "type") == "curve" ) { + var c : Dynamic = deepCopy(d); + c.type = "fxcurve"; + upgradeCurve(c); + var ch : Array = get(c, "children"); + if( ch != null ) c.children = [for( x in ch ) if( x != null ) strip(x)]; + return c; + } + var o : Dynamic = { name: d.name }; + var ch : Array = get(d, "children"); + if( ch != null ) o.children = [for( c in ch ) if( c != null ) strip(c)]; + return o; + } + mirror = @:privateAccess hrt.prefab.Prefab.createFromDynamic(strip(pristine), null, new hrt.prefab.ContextShared(null, false)); + } + var p = mirror; + for( i in n.origin ) { + if( p == null || i >= p.children.length ) return null; + p = p.children[i]; + } + return p; + } + + /** + What the value of curve `n` is read at (`Curve.makeVal`), added to `inputs`: "" for the FX time, else + the name of an FX parameter (a `$param.` remap, a blend on a parameter). Nothing for a constant. + **/ + function collectInputs( n : RawNode, inputs : Array, depth = 0 ) { + if( n == null || depth > 16 || isConstant(n) ) return; + var own = []; + switch( blendMode(n) ) { + case 3: collectInputs(locate(get(n.data, "blendParam")), own, depth + 1); + case 1: + own.push(""); + var p : String = get(n.data, "blendParam"); + if( p != null ) own.push(p); + default: own.push(""); + } + // the shape is read at what the remap is read at + var remap : String = get(n.data, "remapPath"); + var remapCurve = remap == null || StringTools.startsWith(remap, "$param.") ? null : locate(remap); + if( remap != null && (remapCurve != null || StringTools.startsWith(remap, "$param.")) ) { + own.remove(""); + if( remapCurve != null ) + collectInputs(remapCurve, own, depth + 1); + else + own.push(remap.substr("$param.".length)); + } + for( i in own ) + if( inputs.indexOf(i) < 0 ) + inputs.push(i); + } + + /** Keys of a curve and of what it depends on, for the span of a re-bake over its input. **/ + function collectSpan( n : RawNode, span : { min : Float, max : Float, loop : Bool }, ?keyTimes : Array, depth = 0 ) { + if( n == null || depth > 16 ) return; + var d = n.data; + if( get(d, "loop") == true ) span.loop = true; + var remap : String = get(d, "remapPath"); + if( remap != null && !StringTools.startsWith(remap, "$param.") ) { + // the curve is read at the remapped time, the span is the one of the remap curve + collectSpan(locate(remap), span, keyTimes, depth + 1); + return; + } + switch( blendMode(n) ) { + case 3: collectSpan(locate(get(d, "blendParam")), span, keyTimes, depth + 1); + case 1: + for( c in n.curveChildren() ) collectSpan(c, span, keyTimes, depth + 1); + default: + var keys : Array = get(d, "keys"); + if( keys == null || keys.length == 0 ) keys = [{ time: 0. }, { time: 1. }]; + for( k in keys ) { + var t = num(get(k, "time"), CurveKeyDefault); + if( keyTimes != null ) keyTimes.push(t); + if( t < span.min ) span.min = t; + if( t > span.max ) span.max = t; + } + } + } + + /** + Keys fitting `values` (sampled at `times`) within `tol`: the fewest of a linear fit, a smooth one, + and a smooth one broken at the kinks of the samples (`fitWith`). + **/ + static function fitKeys( times : Array, values : Array, tol : Float ) : Array { + // a time sampled twice once + var keep = [for( i in 0...times.length ) i == 0 || times[i] > times[i - 1]]; + times = [for( i in 0...keep.length ) if( keep[i] ) times[i]]; + values = [for( i in 0...keep.length ) if( keep[i] ) values[i]]; + var best : Array = null; + for( mode in 0...3 ) { + var keys = fitWith(times, values, tol, mode); + if( best == null || keys.length < best.length ) best = keys; + } + return best; + } + + /** + A fit adding a key where it misses the most. `mode` 0: linear keys. 1: a cubic between two keys, + the slope of the samples at its keys, handles at a third of the segment in time, so the time stays + linear and the value is the Hermite cubic (`Curve.getVal` reads it at steps of 1/32 of the segment, + linearly between them), checked between the samples too; a line between two consecutive samples, a + jump is a Constant key before it. 2: the same, the tangents broken at the kinks. + **/ + static function fitWith( times : Array, values : Array, tol : Float, mode : Int ) : Array { + var n = times.length; + inline function slope( i : Int ) return (values[i + 1] - values[i]) / (times[i + 1] - times[i]); + // much steeper than the samples around it + var jump = [for( i in 0...n - 1 ) mode > 0 && n > 2 && Math.abs(values[i + 1] - values[i]) > tol + && (i == 0 || Math.abs(slope(i)) > 8 * Math.abs(slope(i - 1))) && (i == n - 2 || Math.abs(slope(i)) > 8 * Math.abs(slope(i + 1)))]; + // a change of slope much sharper than the ones around it + inline function bend( i : Int ) return i < 1 || i > n - 2 ? 0. : Math.abs(slope(i) - slope(i - 1)); + var kink = [for( i in 0...n ) mode == 2 && i > 0 && i < n - 1 && !jump[i] && !jump[i - 1] + && bend(i) * (times[i + 1] - times[i - 1]) > tol * 0.1 && bend(i) > 4 * Math.max(bend(i - 2), bend(i + 2))]; + var mL = [], mR = []; + for( i in 0...n ) { + var l = i > 0 && !jump[i - 1], r = i < n - 1 && !jump[i]; + if( l && r && !kink[i] ) { + var m = (slope(i - 1) * (times[i + 1] - times[i]) + slope(i) * (times[i] - times[i - 1])) / (times[i + 1] - times[i - 1]); + mL.push(m); + mR.push(m); + } else { + mL.push(l ? slope(i - 1) : r ? slope(i) : 0.); + mR.push(r ? slope(i) : l ? slope(i - 1) : 0.); + } + } + function at( a : Int, b : Int, t : Float ) { + var seg = times[b] - times[a]; + if( mode == 0 ) + return values[a] + (values[b] - values[a]) * (t - times[a]) / seg; + inline function h( u : Float ) { + var u2 = u * u, u3 = u2 * u; + return (2 * u3 - 3 * u2 + 1) * values[a] + (u3 - 2 * u2 + u) * seg * mR[a] + (3 * u2 - 2 * u3) * values[b] + (u3 - u2) * seg * mL[b]; + } + var f = (t - times[a]) / seg * 32, k = Math.floor(f); + return hxd.Math.lerp(h(k / 32), h((k + 1) / 32), f - k); + } + var keep = [for( i in 0...n ) i == 0 || i == n - 1 || jump[i] || (i > 0 && jump[i - 1]) || kink[i]]; + var stack = [], prev = 0; + for( i in 1...n ) + if( keep[i] ) { + stack.push(prev); stack.push(i); + prev = i; + } + while( stack.length > 0 ) { + var b = stack.pop(), a = stack.pop(); + if( b == a + 1 ) continue; + var worst = -1, err = tol; + for( i in a...b ) { + var e = i == a ? 0 : Math.abs(at(a, b, times[i]) - values[i]); + if( mode > 0 ) + e = Math.max(e, Math.abs(at(a, b, (times[i] + times[i + 1]) * 0.5) - (values[i] + values[i + 1]) * 0.5)); + if( e > err ) { + err = e; + worst = i == a ? a + 1 : i; + } + } + if( worst >= 0 ) { + keep[worst] = true; + stack.push(a); stack.push(worst); + stack.push(worst); stack.push(b); + } + } + var idx = [for( i in 0...n ) if( keep[i] ) i]; + // a constant curve is one key + if( idx.length == 2 && Math.abs(values[0] - values[n - 1]) <= tol ) + return [obj(["time", times[0], "value", values[0], "mode", 2])]; + var keys = []; + for( j => i in idx ) { + var k : Dynamic = obj(["time", times[i], "value", values[i], "mode", mode == 0 ? 2 : i < n - 1 && jump[i] ? 3 : mL[i] != mR[i] ? 1 : 0]); + if( mode > 0 && j > 0 && idx[j - 1] < i - 1 ) { + var dt = (times[i] - times[idx[j - 1]]) / 3; + k.prevHandle = obj(["dt", -dt, "dv", -mL[i] * dt]); + } + if( mode > 0 && j < idx.length - 1 && idx[j + 1] > i + 1 ) { + var dt = (times[idx[j + 1]] - times[i]) / 3; + k.nextHandle = obj(["dt", dt, "dv", mR[i] * dt]); + } + keys.push(k); + } + return keys; + } + + /** A plain curve whose keys all have the same value **/ + static function isConstant( n : RawNode ) { + if( blendMode(n) != 0 || get(n.data, "remapPath") != null ) return false; + var keys : Array = get(n.data, "keys"); + if( keys == null || keys.length == 0 ) return false; + for( k in keys ) { + if( num(get(k, "value"), CurveKeyDefault) != num(get(keys[0], "value"), CurveKeyDefault) ) return false; + for( h in [get(k, "prevHandle"), get(k, "nextHandle")] ) + if( h != null && num(get(h, "dv"), CurveKeyDefault) != 0 ) return false; + } + return true; + } + + // ---------------------------------------------------------------------------------------- + // FX + + /** The outermost FX of the file get their animations upgraded. **/ + function fxPass( n : RawNode ) { + var is2D = n.isA(hrt.prefab.fx.FX2D); + if( n.isA(hrt.prefab.fx.FX) || is2D ) { + var params : Array = get(n.data, "parameters"); + parameters = new Map(); + if( params != null ) + for( p in params ) + parameters.set(get(p, "name"), num(get(p, "def"), 0)); + // the default duration of an FX + fxDuration = num(get(n.data, "duration"), 5.0); + prepareEmitters(n); + process(n, is2D, false); + markAll(n, n, true); + return; + } + for( c in n.children.copy() ) + fxPass(c); + } + + /** + The emitters upgrade their own curves next: the curves that reference the ones they bake are + made independent first (they would read the new values otherwise). + **/ + function prepareEmitters( n : RawNode ) { + if( Migrate.isEmitter(n.data) ) { + for( c in n.children ) + if( isLegacy(c) && isEmitterAnim(c.name) ) { + var us = users.get(c); + if( us != null ) + for( u in us.copy() ) { + warn(u, "reference to a curve of an emitter, made a copy of it"); + dereference(u); + } + } + } + for( c in n.children ) + prepareEmitters(c); + } + + /** The old names of the curves the emitters bake: settings, spawn params and over time settings. **/ + static var EMITTER_ANIMS = ["emitrate", "emitratemin", "emitratemax", "burstparticlecount", "emitangle", "emitradius", "emitangle1", "emitangle2", + "emitwidth", "emitheight", "inststartspeed", "inststartworldspeed", "instscaleovertime", "instorbitspeedovertime"]; + + static function isEmitterAnim( name : String ) { + return EMITTER_ANIMS.indexOf(name.split(":")[0].toLowerCase()) >= 0; + } + + function process( n : RawNode, is2D : Bool, inEmitter : Bool ) { + // what its curves animate, and from which values, depends on its class + if( n.cls == null && Lambda.exists(n.children, c -> c.isCurve && c.enabled()) ) + fail(n, 'prefab type ${n.type} unknown to this build: its curves are not upgraded (the migration must be built with the prefab classes of the game, see hide/migrate.hxml)'); + if( !is2D && !inEmitter && n.isA(hrt.prefab.Object3D) ) + upgradeObject3D(n); + if( is2D && n.isA(hrt.prefab.Object2D) ) + upgradeObject2D(n); + if( (n.isA(hrt.prefab.Shader) && particleEmitter(n) == null) || n.isA(hrt.prefab.rfx.ScreenShaderGraph) ) + upgradeShader(n); + var em = n.isA(hrt.prefab.fx.Emitter); + // a parent may be inserted above a child, which is then processed through it + for( c in n.children.copy() ) { + var cur = c.parent == n ? c : c.parent; + process(cur, is2D, inEmitter || em); + } + } + + /** Mirrors `EmitterHelper.getParticleEmitter`: the shaders under an emitter are evaluated over the particle life. **/ + static function particleEmitter( n : RawNode ) : RawNode { + var p = n.parent; + while( p != null ) { + if( p.isA(hrt.prefab.fx.Emitter) ) return p; + if( p.isA(hrt.prefab.l3d.Trails) ) return null; + p = p.parent; + } + return null; + } + + /** The enabled curves named `name` or `name:...`, like `Curve.getCurves` **/ + static function group( n : RawNode, name : String ) : Array { + return [for( c in n.children ) if( c.isCurve && c.enabled() && sameName(c.name.split(":")[0], name) ) c]; + } + + static function isTransformCurve( c : RawNode ) { + if( !c.isCurve ) return false; + var prefix = c.name.split(":")[0]; + return sameName(prefix, "position") || sameName(prefix, "scale") || sameName(prefix, "rotation"); + } + + /** Like `BaseFXTools.vectorAnimation`: the curve of each component, `uniform` when one curve is all of them. **/ + static function channels( curves : Array, name : String, dim : Int ) : Channels { + if( curves.length == 1 && curves[0].name == name ) + return { uniform: curves[0], comps: [for( _ in 0...dim ) curves[0]] }; + return { uniform: null, comps: [for( i in 0...dim ) { + var suffix = ":" + ["x", "y", "z", "w"][i]; + Lambda.find(curves, c -> StringTools.endsWith(c.name.toLowerCase(), suffix)); + }] }; + } + + static var COMPONENTS = ["x", "y", "z", "w"]; + + /** Applies `a[i] * value + b[i]` to the curves of a vector property **/ + function bakeVector( owner : RawNode, prop : String, ch : Channels, a : Array, b : Array, kind : String ) { + applyMap(owner, prop, ch, [for( i in 0...a.length ) { src: ch.comps[i] == null ? null : i, a: a[i], b: b[i] }], kind); + } + + function upgradeObject3D( n : RawNode ) { + var d = n.data; + var pos = [for( f in ["x", "y", "z"] ) num(fieldOf(n, f), 0)]; + var scale = [for( f in ["scaleX", "scaleY", "scaleZ"] ) num(fieldOf(n, f), 1)]; + var rot = [for( f in ["rotationX", "rotationY", "rotationZ"] ) num(fieldOf(n, f), 0)]; + + // position: offset of the base position + var gp = group(n, "position"); + if( anyLegacy(gp) ) + bakeVector(n, "position", channels(gp, "position", 3), [1, 1, 1], pos, "position"); + + // local position and rotation of the leaves: no longer supported + var local = [for( c in n.children ) if( c.isCurve && (sameName(c.name.split(":")[0], "localPosition") || sameName(c.name.split(":")[0], "localRotation")) ) c]; + if( local.length > 0 ) { + var dataChildren : Array = get(d, "children"); + for( c in local ) { + n.children.remove(c); + dataChildren.remove(c.data); + } + warn(n, "local position and rotation animations are no longer supported, removed"); + change(n, "local animation removed"); + } + + var gs = group(n, "scale"), gr = group(n, "rotation"); + if( !anyLegacy(gs) && !anyLegacy(gr) ) + return; + var cs = gs.length == 0 ? null : channels(gs, "scale", 3); + var cr = gr.length == 0 ? null : channels(gr, "rotation", 3); + + // old transform: base scale * base rotation * scale(t) * rotation(t) (row vectors, the base first) + // the animated scale moves before the base rotation: base rotation * scale(t) = scale'(t) * base rotation + var scaleMap : Array = null; + if( cs != null ) { + var perm = TransformMath.scalePermutation(rot, classes(cs)); + if( perm != null ) + scaleMap = [for( i in 0...3 ) { src: cs.comps[perm[i]] == null ? null : perm[i], a: scale[i], b: 0. }]; + } + // base rotation * rotation(t) as euler angles affine in the curves, from an euler triple of the base rotation + var form = cr == null ? null : TransformMath.rotationForm(rot, classes(cr)); + var shear = cs != null && scaleMap == null; + var problem = shear ? "scale through the base rotation (shear)" : cr != null && form == null ? "rotation the euler angles cannot follow" : null; + if( problem != null ) { + var kept = noParent(n); + if( kept == null && !shear ) { + insertParent(n, problem); + return; + } + // the parent would show the shear the old runtime dropped, or there is none: sampled + var fit = resample(n, shear ? cs : null, cr, scale, rot, shear); + if( fit.problem != null && kept == null ) { + insertParent(n, fit.problem); + return; + } + writeResample(n, fit, kept == null ? problem : '$problem, $kept'); + if( !shear && cs != null ) + applyMap(n, "scale", cs, scaleMap, "scale, with rotation"); + return; + } + if( cr != null ) { + var b = form.base; + if( b[0] != rot[0] || b[1] != rot[1] || b[2] != rot[2] ) { + // the same base rotation, as the euler triple the curves go from + for( i => f in ["rotationX", "rotationY", "rotationZ"] ) RawNode.setField(d, f, b[i]); + change(n, "base rotation as another euler triple"); + } + var rotMap = [for( j in 0...3 ) { + var k = form.perm[j]; + cr.comps[k] == null ? { src: null, a: 0., b: b[j] } : { src: k, a: 360. * form.sign[j], b: b[j] }; + }]; + applyMap(n, "rotation", cr, rotMap, rot[0] == 0 && rot[1] == 0 && rot[2] == 0 ? "rotation" : "rotation, base rotation folded"); + } + if( cs != null ) + applyMap(n, "scale", cs, scaleMap, cr == null ? "scale" : "scale, with rotation"); + } + + /** The class of each component of a vector property for `TransformMath`: the same for the same curve or curves of the same data, -1 for none **/ + static function classes( ch : Channels ) : Array { + function key( c : RawNode ) { + var d = Reflect.copy(c.data); + Reflect.deleteField(d, "name"); + return haxe.Json.stringify(d); + } + var keys = [for( c in ch.comps ) c == null ? null : key(c)]; + return [for( i in 0...3 ) keys[i] == null ? -1 : keys.indexOf(keys[i])]; + } + + /** + Why the object `n` cannot get a parent (`insertParent`), null when it can: the parent takes its + animation and the object keeps its world transform, but what reads the transform of its parent + object, sets its own rotation or reads its direct children would then see another one. + **/ + function noParent( n : RawNode ) : String { + if( n.parent == null || n.isA(hrt.prefab.fx.FX) ) + return "the root of the FX cannot get a parent"; + // particles in the space of the parent object, its scale and the local position of the emitter + if( n.isA(hrt.prefab.fx.Emitter) ) + return "an emitter reads its parent object"; + if( n.isA(hrt.prefab.fx.Swarm) ) + return "a swarm reads its parent object"; + // the parent would turn the rotation they set + if( n.type == "billboard" ) + return "a billboard sets its own rotation"; + var lock : Array = get(n.data, "lockAxis"); + if( n.isA(hrt.prefab.fx.LookAt) && lock != null && Lambda.exists(lock, v -> v != 0) ) + return "a look at with a lock axis sets its own rotation"; + // templates and shader holders: their direct children + for( cl in ([hrt.prefab.fx.ShaderTarget, hrt.prefab.fx.Swarm, hrt.prefab.fx.gpuemitter.GPUEmitter] : Array>) ) + if( n.parent.isA(cl) ) + return 'the ${n.parent.type} reads its direct children'; + return null; + } + + /** + Sets the curves of a vector property from the old ones: component `i` becomes `a * old[src] + b`, + or the value of the prefab (`b`, no curve) when `src` is null. + **/ + function applyMap( owner : RawNode, prop : String, ch : Channels, map : Array, kind : String ) { + var u = ch.uniform; + if( u != null ) { + if( !isLegacy(u) ) return; + var same = true; + for( m in map ) if( m.src == null || m.a != map[0].a || m.b != map[0].b ) same = false; + if( same ) { + bakeLinear(u, map[0].a, map[0].b); + markUpgraded(u); + change(u, kind + ", uniform"); + return; + } + // the uniform curve stays as the shape of its components + var any = false; + for( i in 0...map.length ) + if( map[i].src != null ) { + linearCopy(u, owner, prop + ":" + COMPONENTS[i], map[i].a, map[i].b); + any = true; + } + if( !any && (!users.exists(u) || users.get(u).length == 0) ) RawNode.setField(u.data, "enabled", false); + markUpgraded(u); + change(u, kind + ", uniform split"); + return; + } + var orig = [for( c in ch.comps ) c == null ? null : deepCopy(c.data)]; + for( i in 0...map.length ) { + var m = map[i]; + var node = ch.comps[i]; + if( m.src == i ) { + if( isLegacy(node) ) { + bakeLinear(node, m.a, m.b); + markUpgraded(node); + change(node, kind); + } + continue; + } + if( m.src == null ) { + // the value of the prefab: the curve is not read anymore + if( node != null && isLegacy(node) ) { + RawNode.setField(node.data, "enabled", false); + markUpgraded(node); + change(node, kind + ", moved to another component"); + } + continue; + } + // another component: a copy of its old curve + var d : Dynamic = deepCopy(orig[m.src]); + d.name = node != null ? node.data.name : freeName(owner, prop + ":" + COMPONENTS[i]); + if( node != null ) { + var en : Dynamic = get(node.data, "enabled"); + if( en == null ) Reflect.deleteField(d, "enabled") else d.enabled = en; + } + var target = node; + if( target == null ) + target = owner.addChild(d); + else + replaceData(target, d); + indexUsers(target); + bakeLinear(target, m.a, m.b, false); + markUpgraded(target); + change(target, kind + ", from another component"); + } + } + + /** Replaces the data of a curve, the curves that referenced it get its old data. **/ + function replaceData( node : RawNode, d : Dynamic ) { + var us = users.get(node); + if( us != null ) + for( u in us.copy() ) dereference(u); + RawNode.replaceFields(node.data, d); + node.rebuildChildren(); + } + + /** + Samples the old transform of an object and fits the rotation (and the scale when `withScale`) + with linear keys, over what its curves are read at: the FX time, or the FX parameter they are all + remapped by (the keys are then remapped by it too). `problem` tells why the keys cannot stand for + the old transform, null when they do (checked against the old transform over the whole span). + Curves read at several inputs are sampled over one of them, the others at their default (`approx`). + **/ + function resample( n : RawNode, cs : Channels, cr : Channels, scale : Array, rot : Array, withScale : Bool ) : Resample { + var involved : Array = []; + for( ch in [cs, cr] ) + if( ch != null ) + for( c in ch.comps ) + if( c != null && involved.indexOf(c) < 0 ) + involved.push(c); + // the input the keys are sampled over: the FX time, or the FX parameter all the curves are read at + var inputs = []; + for( c in involved ) + collectInputs(c, inputs); + var input = inputs.length == 0 || inputs.indexOf("") >= 0 ? "" : inputs[0]; + inline function inputName( i : String ) return i == "" ? "the FX time" : "parameter " + i; + var approx = inputs.length < 2 ? null : 'depends on ${[for( i in inputs ) inputName(i)].join(", ")}: sampled over ${inputName(input)}, the others at the default of the parameters'; + var span = { min: 0., max: 0., loop: false }; + var keyTimes = []; + var periods = []; + var varyingOnce = false; + for( c in involved ) { + var cspan = { min: 0., max: 0., loop: false }; + collectSpan(c, cspan, keyTimes); + if( cspan.min < span.min ) span.min = cspan.min; + if( cspan.max > span.max ) span.max = cspan.max; + if( cspan.loop ) { + span.loop = true; + if( periods.indexOf(cspan.max) < 0 ) periods.push(cspan.max); + } else if( !isConstant(c) ) + varyingOnce = true; + } + // all the curves loop: the re-bake loops over their common period, else it covers the FX + var period = TransformMath.commonPeriod(periods); + var loop = input == "" && span.loop && !varyingOnce && period > 0 && span.min >= 0; + if( input != "" ) { + // the range of a blend parameter and its default, and beyond, where the curves are read clamped: the + // keys then span more than `Curve.BAKE_MAX_DURATION`, the runtime does not smooth their steps + var def = parameters.get(input) ?? 0.; + span.min = Math.min(span.min, Math.min(0, def)) - 0.5; + span.max = Math.max(span.max, Math.max(1, def)) + 0.5; + } else if( loop ) { + span.min = 0; + span.max = period; + } else { + var duration = fxDuration > 0 ? fxDuration : 5.0; + if( span.loop && span.max < duration ) span.max = duration; + span.max += 1; + } + + var ev = new Evaluator(); + for( k => v in parameters ) ev.parameters.set(k, v); + function val( c : RawNode ) : Value { + if( c == null ) return null; + var p = Std.downcast(mirrorOf(c), hrt.prefab.fx.Curve); + return p?.makeVal(); + } + var sv = cs == null ? [null, null, null] : [for( c in cs.comps ) val(c)]; + var rv = cr == null ? [null, null, null] : [for( c in cr.comps ) val(c)]; + + var times = []; + var steps = Math.ceil((span.max - span.min) / SAMPLE_STEP); + if( steps > 20000 ) steps = 20000; + if( steps < 1 ) steps = 1; + for( i in 0...steps + 1 ) + times.push(span.min + (span.max - span.min) * i / steps); + // the keys, and around them for the steps + for( t in keyTimes ) + for( x in [t - 1e-5, t, t + 1e-5] ) + if( x > span.min && x < span.max ) + times.push(x); + times.sort(Reflect.compare); + + var base = new h3d.Matrix(); + base.initScale(withScale ? scale[0] : 1, withScale ? scale[1] : 1, withScale ? scale[2] : 1); + base.rotate(hxd.Math.degToRad(rot[0]), hxd.Math.degToRad(rot[1]), hxd.Math.degToRad(rot[2])); + var m = new h3d.Matrix(), q = new h3d.Quat(), tmp = new h3d.Matrix(), sc = new h3d.Vector(); + // at `x` of the input + function rawMatrix( x : Float, out : h3d.Matrix ) { + var t = x; + if( input != "" ) { + ev.parameters.set(input, x); + t = 0; + } + inline function f( v : Value, def : Float ) return v == null ? def : ev.getFloat(v, t); + var s = withScale ? [f(sv[0], 1), f(sv[1], 1), f(sv[2], 1)] : [1., 1., 1.]; + out.initScale(s[0], s[1], s[2]); + out.rotate(f(rv[0], 0) * Math.PI * 2, f(rv[1], 0) * Math.PI * 2, f(rv[2], 0) * Math.PI * 2); + out.multiply(base, out); + return s; + } + // the old runtime set it with h3d.scene.Object.setTransform, which keeps a scale and a quaternion: a + // shear is lost, and the rotation of the previous frame stays while a scale is 0 + function rotationAt( t : Float, depth : Int ) { + rawMatrix(t, tmp); + var sc = tmp.getScale(); + if( Math.abs(sc.x) > hxd.Math.EPSILON && Math.abs(sc.y) > hxd.Math.EPSILON && Math.abs(sc.z) > hxd.Math.EPSILON ) { + tmp.prependScale(1 / sc.x, 1 / sc.y, 1 / sc.z); + q.initRotateMatrix(tmp); + } else if( depth < 60 ) + rotationAt(t - 1 / 60, depth + 1); + else + q.initRotation(hxd.Math.degToRad(rot[0]), hxd.Math.degToRad(rot[1]), hxd.Math.degToRad(rot[2])); + } + // its scale and rotation, `out`: the quaternion of a sheared matrix is not normalized, the runtime then + // showed a transform that is not a scale and a rotation, the closest one is with the quaternion normalized + var loss = 0.; + function oldTransform( t : Float, out : h3d.Matrix ) { + rawMatrix(t, out); + sc.load(out.getScale()); + rotationAt(t, 0); + loss = Math.max(loss, Math.abs(q.lengthSq() - 1)); + q.normalize(); + q.toMatrix(out); + } + // the closest scale and euler angles, the euler angles closest to `prevR` and the scales to `prevS`: the + // scales getScale gives all change sign when one goes through 0, the same transform as two of them + // changing sign with a half turn about the third axis + function sampleAt( t : Float, prevR : Array, prevS : Array ) : { s : Array, r : Array, error : Float, m : h3d.Matrix } { + oldTransform(t, m); + var s = [sc.x, sc.y, sc.z]; + function dist( v : Array ) { + var d = 0.; + for( i in 0...3 ) d += (v[i] - prevS[i]) * (v[i] - prevS[i]); + return d; + } + var turn = -1; + for( k in 0...3 ) + if( dist([for( i in 0...3 ) i == k ? s[i] : -s[i]]) < dist(turn < 0 ? s : [for( i in 0...3 ) i == turn ? s[i] : -s[i]]) ) + turn = k; + if( turn >= 0 ) { + var f = [for( i in 0...3 ) i == turn ? 1. : -1.]; + m.prependScale(f[0], f[1], f[2]); + s = [for( i in 0...3 ) s[i] * f[i]]; + } + var d = TransformMath.decompose(m, prevR, [1., 1., 1.]); + d.s = [d.s[0] * s[0], d.s[1] * s[1], d.s[2] * s[2]]; + d.m.prependScale(s[0], s[1], s[2]); + return d; + } + // samples, refined where the linear keys between two of them would miss the transform + var samples = []; + var prev = { r: [for( r in rot ) hxd.Math.degToRad(r)], s: withScale ? scale : [1., 1., 1.] }; + for( t in times ) { + var d = sampleAt(t, prev.r, prev.s); + prev = d; + samples.push({ t: t, d: d }); + } + var rtol = hxd.Math.degToRad(ROTATION_TOLERANCE); + function refine( a : { t : Float, d : Dynamic }, b : { t : Float, d : Dynamic }, depth : Int, out : Array<{ t : Float, d : Dynamic }> ) { + if( depth > 0 && b.t - a.t > 1e-6 ) { + var tm = (a.t + b.t) * 0.5; + var dm = sampleAt(tm, a.d.r, a.d.s); + var bad = false; + for( i in 0...3 ) { + if( Math.abs(dm.r[i] - (a.d.r[i] + b.d.r[i]) * 0.5) > rtol ) bad = true; + var sv = (a.d.s[i] + b.d.s[i]) * 0.5; + if( withScale && Math.abs(dm.s[i] - sv) > SCALE_TOLERANCE * Math.max(1, Math.abs(scale[i])) ) bad = true; + } + if( bad ) { + var mid = { t: tm, d: (dm : Dynamic) }; + refine(a, mid, depth - 1, out); + out.push(mid); + refine(mid, b, depth - 1, out); + } + } + } + var all = [samples[0]]; + for( i in 1...samples.length ) { + refine(samples[i - 1], samples[i], 10, all); + all.push(samples[i]); + } + times = [for( x in all ) x.t]; + var outS = [for( i in 0...3 ) [for( x in all ) (x.d.s[i] : Float)]]; + var outR = [for( i in 0...3 ) [for( x in all ) hxd.Math.radToDeg(x.d.r[i])]]; + + var scaleKeys = [], rotKeys = []; + for( i in 0...3 ) { + scaleKeys.push(withScale ? fitKeys(times, outS[i], SCALE_TOLERANCE * Math.max(1, Math.abs(scale[i]))) : null); + rotKeys.push(fitKeys(times, outR[i], ROTATION_TOLERANCE)); + } + var fit : Resample = { cs: cs, cr: cr, scale: scale, rot: rot, withScale: withScale, loop: loop, input: input, scaleKeys: scaleKeys, rotKeys: rotKeys, + problem: null, approx: approx, nonRigid: loss }; + // the keys against the old transform, between the samples and around the keys + var check = []; + var t = span.min - 0.5; + // a re-bake of curves that keep looping holds until its end + while( t <= span.max + (span.loop && !loop ? 0 : 0.5) ) { + check.push(t); + t += SAMPLE_STEP * 0.5; + } + for( k in keyTimes ) { + check.push(k - 1e-4); + check.push(k + 1e-4); + } + // against the closest scale and rotation, the rest is reported as a loss + var err = fitError(fit, check, (t, out) -> out.load(sampleAt(t, [0., 0., 0.], [1., 1., 1.]).m)); + if( err > MATRIX_TOLERANCE ) + fit.problem = 'rotation the euler angles cannot follow (error ${Math.round(err * 1e4) / 1e4} with sampled keys)'; + return fit; + } + + /** The worst error of the re-baked keys against the old transform at `times`, relative to the scale of the matrix rows. **/ + function fitError( fit : Resample, times : Array, oldMatrix : (Float, h3d.Matrix) -> Void ) : Float { + var ev = new Evaluator(); + function curve( keys : Array, base : Float ) : Value { + if( keys == null ) return VConst(base); + var isBase = keys.length == 1 && Math.abs(keys[0].value - base) <= 1e-12; + if( isBase ) return VConst(base); + var d : Dynamic = { type: "fxcurve", name: "c", keys: deepCopy(keys) }; + if( fit.loop ) d.loop = true; + var c : hrt.prefab.fx.Curve = cast @:privateAccess hrt.prefab.Prefab.createFromDynamic(d, null, new hrt.prefab.ContextShared(null, false)); + return c.makeVal(); + } + var withScale = fit.withScale, scale = fit.scale, rot = fit.rot, sk = fit.scaleKeys, rk = fit.rotKeys; + var sv = [for( i in 0...3 ) withScale ? curve(sk[i], scale[i]) : VConst(scale[i])]; + var rv = [for( i in 0...3 ) curve(rk[i], rot[i])]; + var a = new h3d.Matrix(), b = new h3d.Matrix(); + var worst = 0.; + for( t in times ) { + oldMatrix(t, a); + if( !withScale ) a.multiply(TransformMath.scaleMatrix(scale), a); + b.initScale(ev.getFloat(sv[0], t), ev.getFloat(sv[1], t), ev.getFloat(sv[2], t)); + b.rotate(hxd.Math.degToRad(ev.getFloat(rv[0], t)), hxd.Math.degToRad(ev.getFloat(rv[1], t)), hxd.Math.degToRad(ev.getFloat(rv[2], t))); + var rows = [[a._11, a._12, a._13, b._11, b._12, b._13], [a._21, a._22, a._23, b._21, b._22, b._23], [a._31, a._32, a._33, b._31, b._32, b._33]]; + for( r in rows ) { + var n = Math.max(1e-6, Math.sqrt(r[0] * r[0] + r[1] * r[1] + r[2] * r[2])); + for( j in 0...3 ) { + var e = Math.abs(r[j] - r[j + 3]) / Math.max(1, n); + if( e > worst ) worst = e; + } + } + } + return worst; + } + + /** Writes a sampled re-bake, `why` the object has no exact form, warned to list the object **/ + function writeResample( n : RawNode, fit : Resample, why : String ) { + var withScale = fit.withScale, scale = fit.scale, rot = fit.rot; + for( i in 0...3 ) { + if( withScale ) + setChannel(n, fit.cs, "scale", i, fit.scaleKeys[i], scale[i], SCALE_TOLERANCE * Math.max(1, Math.abs(scale[i])), fit.loop, fit.input); + setChannel(n, fit.cr, "rotation", i, fit.rotKeys[i], rot[i], ROTATION_TOLERANCE, fit.loop, fit.input); + } + var msg = '$why: sampled to what the old runtime showed'; + if( fit.input != "" ) + msg += ' over parameter ${fit.input}'; + if( fit.approx != null ) + msg += ', approximated (${fit.approx})'; + if( fit.problem != null ) + msg += ', approximated (${fit.problem})'; + if( fit.nonRigid > MATRIX_TOLERANCE ) + msg += ', the quaternion of the sheared matrix normalized (off by ${Math.round(fit.nonRigid * 1e4) / 1e4})'; + warn(n, msg); + change(n, withScale ? "rotation and scale, sampled" : "rotation, sampled"); + } + + /** Sets the curve of component `i` of `prop` to `keys` over `input` (see `Resample`), or none when it is the base value. **/ + function setChannel( n : RawNode, ch : Channels, prop : String, i : Int, keys : Array, base : Float, tol : Float, loop : Bool, input : String ) { + var isBase = keys.length == 1 && Math.abs(keys[0].value - base) <= tol; + var cur = ch == null || ch.uniform != null ? null : ch.comps[i]; + if( ch != null && ch.uniform != null && i == 0 ) { + // the components get their own curves, the uniform one is not read anymore: disable it + // unless something references it + var u = ch.uniform; + if( !users.exists(u) || users.get(u).length == 0 ) + RawNode.setField(u.data, "enabled", false); + markUpgraded(u); + } + if( isBase && cur == null ) + return; + if( cur != null && users.exists(cur) && users.get(cur).length > 0 ) { + // keep the shared curve for its users + for( u in users.get(cur).copy() ) + dereference(u); + } + var d : Dynamic = cur != null ? cur.data : newCurve(freeName(n, prop + ":" + COMPONENTS[i])); + if( isBase ) + keys = [obj(["time", 0., "value", base, "mode", 2])]; + for( f in ["blendMode", "blendParam", "remapPath", "scale", "offset", "loop", "previewKeys", "children", "keyMode"] ) + Reflect.deleteField(d, f); + RawNode.setField(d, "keys", keys); + if( loop && !isBase ) RawNode.setField(d, "loop", true); + if( input != "" && !isBase ) RawNode.setField(d, "remapPath", "$param." + input); + upgraded.set(d, true); + if( cur == null ) + n.addChild(d); + else + cur.children = []; + } + + /** + The object `n` (its old animation has no exact form on its own transform) becomes the only + child of a new object, which takes its name, its place among its siblings, its position and + its transform curves: old transform = base scale * base rotation * scale(t) * rotation(t), + then the position, which is the child (base scale and rotation, no position) under the + parent (scale(t), rotation(t), position): every curve of the parent is an exact linear bake. + The object keeps everything else and is renamed `_base`; the paths of the file through + it are updated. + **/ + function insertParent( n : RawNode, why : String ) { + var d = n.data; + var oldChildren = n.children.copy(); + var parentNode = n.parent; + var p : Dynamic = obj(["type", "object", "name", d.name]); + for( f in ["x", "y", "z"] ) { + var v : Dynamic = get(d, f); + if( v != null ) { + Reflect.setField(p, f, v); + Reflect.deleteField(d, f); + } + } + var animated = [for( c in n.children ) if( isTransformCurve(c) ) c]; + var content = [for( c in n.children ) if( !isTransformCurve(c) ) c]; + var childName = n.name + BASE_SUFFIX; + var k = 1; + while( Lambda.exists(animated, c -> c.name == childName) ) + childName = n.name + BASE_SUFFIX + (k++); + p.children = [d].concat([for( c in animated ) c.data]); + var dataChildren : Array = get(d, "children"); + if( dataChildren != null ) { + for( c in animated ) dataChildren.remove(c.data); + if( dataChildren.length == 0 ) Reflect.deleteField(d, "children"); + } + + // the tree: the parent takes the place of the object + var pn = new RawNode(); + pn.data = p; + pn.type = "object"; + pn.cls = RawNode.classOf("object"); + pn.isCurve = false; + pn.name = n.name; + pn.parent = parentNode; + var arr : Array = get(parentNode.data, "children"); + arr[arr.indexOf(d)] = p; + parentNode.children[parentNode.children.indexOf(n)] = pn; + n.parent = pn; + n.children = content; + pn.children = [n].concat(animated); + for( c in animated ) c.parent = pn; + n.rename(childName); + rewritePaths(pn, n, oldChildren); + + // the transform curves of the parent: exact bakes + var pos = [num(get(p, "x"), 0), num(get(p, "y"), 0), num(get(p, "z"), 0)]; + var gp = group(pn, "position"); + if( anyLegacy(gp) ) + bakeVector(pn, "position", channels(gp, "position", 3), [1, 1, 1], pos, "position"); + var gr = group(pn, "rotation"); + if( anyLegacy(gr) ) + bakeVector(pn, "rotation", channels(gr, "rotation", 3), [360, 360, 360], [0, 0, 0], "rotation, under a parent"); + for( c in group(pn, "scale") ) { + if( isLegacy(c) ) change(c, "scale, under a parent"); + markUpgraded(c); + } + change(pn, "parent object inserted"); + report.parents.push({ path: pn.path(), child: childName, reason: why }); + } + + /** + The paths of the file that go through the object `child`, now under `parent`: the curve + references and time remaps (prefab paths from the root), the look at targets and the + constraints (object paths, see `fixObjectPath`). + **/ + function rewritePaths( parent : RawNode, child : RawNode, oldChildren : Array ) { + function fix( path : String ) : String { + if( path == null ) return null; + var p = root; + var out = []; + for( part in path.split(".") ) { + // under the parent, a part names one of the old children of the object + var found = findChild(p == parent ? oldChildren : p.children, part); + if( found == null ) return path; + if( p == parent ) { + if( found.parent == child ) out.push(child.uniqueName()); + out.push(found.uniqueName()); + } else + out.push(part); + p = found; + } + return out.join("."); + } + function walk( n : RawNode ) { + var d = n.data; + for( f in (n.isCurve ? ["blendParam", "remapPath"] : ["target", "object"]) ) { + var v : Dynamic = get(d, f); + if( !Std.isOfType(v, String) || StringTools.startsWith(v, "$param.") ) continue; + // the object a constraint moves is the parent, which carries the animation + var nv = n.isCurve ? fix(v) : fixObjectPath(v, parent, child, !(n.isA(hrt.prefab.l3d.Constraint) && f == "object")); + if( nv != v ) { + Reflect.setField(d, f, nv); + change(n, '$f path through the object moved under a parent'); + } + } + for( c in n.children ) walk(c); + } + walk(root); + } + + /** + An object path (`Prefab.locateObject`: object names from the scene root, the FX object first, a name + that is not a child searched below) that goes through `parent`, now the parent of `child`, goes through + `child`; one that names `parent` itself names `child` when `toChild`, the object whose world transform + is the one of the old object. + **/ + function fixObjectPath( path : String, parent : RawNode, child : RawNode, toChild : Bool ) : String { + function search( n : RawNode, name : String ) : RawNode { + if( n.name == name && !n.isCurve ) return n; + for( c in n.children ) { + var f = search(c, name); + if( f != null ) return f; + } + return null; + } + var parts = path.split("."); + var cur : RawNode = null; + var out = []; + for( i => part in parts ) { + var found = cur == null ? search(root, part) : Lambda.find(cur.children, c -> c.name == part && !c.isCurve) ?? search(cur, part); + if( found == null ) return path; + out.push(part); + if( found == parent && (toChild || i < parts.length - 1) ) { + out.push(child.name); + found = child; + } + cur = found; + } + return out.join("."); + } + + function upgradeObject2D( n : RawNode ) { + var gp = group(n, "position"); + if( anyLegacy(gp) ) + bakeVector(n, "position", channels(gp, "position", 2), [1, 1], [num(fieldOf(n, "x"), 0), num(fieldOf(n, "y"), 0)], "2d position"); + var gs = group(n, "scale"); + if( anyLegacy(gs) ) + bakeVector(n, "scale", channels(gs, "scale", 2), [num(fieldOf(n, "scaleX"), 1), num(fieldOf(n, "scaleY"), 1)], [0, 0], "2d scale"); + var gr = group(n, "rotation"); + if( anyLegacy(gr) ) { + // the old code added the rotation of the prefab, in degrees, to the radians of the curve + var r = num(fieldOf(n, "rotation"), 0); + bakeVector(n, "rotation", channels(gr, "rotation", 1), [360], [r != 0 ? hxd.Math.radToDeg(r) : 0], r != 0 ? "2d rotation, base read as radians" : "2d rotation"); + } + } + + /** The last field of the shader `props` named `low` in lower case, the runtime finds the curves case insensitive **/ + static function propName( props : Dynamic, low : String ) : String { + var name = null; + if( props != null ) + for( f in Reflect.fields(props) ) + if( f.toLowerCase() == low ) + name = f; + return name; + } + + /** + The value of shader param `name` the old `makeShaderParams` started from: the stored one (`props`), + else the field of the prefab of that name. Null when there is neither: it was then the default of + the param in the shader, whose definition is not known here. + **/ + static function shaderValue( n : RawNode, name : String ) : Dynamic { + var props : Dynamic = get(n.data, "props"); + var f = propName(props, name.toLowerCase()); + var prop : Dynamic = f == null ? null : Reflect.field(props, f); + return prop ?? fieldOf(n, f ?? name); + } + + static function isNumber( v : Dynamic ) return Std.isOfType(v, Float) || Std.isOfType(v, Int); + + function unknownBase( n : RawNode, name : String ) { + fail(n, 'shader param $name: no stored value nor field, its base value is the default of the param in the shader, which the migration does not know: not upgradable, its curve is kept as the value'); + } + + /** + A float param was `curve * value` (the value when it is a number, 1 for another value). + The kind of a param comes from its value: the shader definition is not known. + **/ + function upgradeShader( n : RawNode ) { + var seen = new Map(); + for( c in n.children ) { + if( !c.isCurve || !c.enabled() || !isLegacy(c) ) continue; + var prefix = c.name.split(":")[0]; + var low = prefix.toLowerCase(); + if( seen.exists(low) ) continue; + seen.set(low, true); + var value : Dynamic = shaderValue(n, prefix); + if( Std.isOfType(value, Array) ) + continue; // vectors and colors come from their curves + // the curve of a float: the first enabled one with the name of the param + var curve = Lambda.find(n.children, x -> x.isCurve && x.enabled() && sameName(x.name, prefix)); + if( curve == null || !isLegacy(curve) ) continue; + if( value == null ) { + var hasComponents = Lambda.exists(n.children, x -> x.isCurve && x.enabled() && x.name.indexOf(":") >= 0 && sameName(x.name.split(":")[0], prefix)); + if( !hasComponents ) + unknownBase(curve, prefix); + continue; + } + var k : Float = isNumber(value) ? value : 1; + bakeLinear(curve, k, 0); + change(curve, k == 1 ? "shader float, value 1" : "shader float"); + } + } + + // ---------------------------------------------------------------------------------------- + // Emitters + + static var EMITTER_3D : Array = [ + { name: "lifeTime", old: "lifeTime", group: Spawn, def: [1.] }, + { name: "partWorldAcceleration", old: "instWorldAcceleration", group: Particle, def: [0., 0., 0.] }, + { name: "partSpeed", old: "instSpeed", group: Particle, def: [0., 0., 0.] }, + { name: "partWorldSpeed", old: "instWorldSpeed", group: Particle, def: [0., 0., 0.] }, + { name: "startSpeed", old: "instStartSpeed", group: Spawn, def: [0., 0., 0.] }, + { name: "startWorldSpeed", old: "instStartWorldSpeed", group: Spawn, def: [0., 0., 0.] }, + { name: "partOrbitSpeed", old: "instOrbitSpeed", group: Particle, def: [0., 0., 0.] }, + { name: "orbitSpeedMultiplier", old: "instOrbitSpeedOverTime", group: OverTime, def: [1.] }, + { name: "partAcceleration", old: "instAcceleration", group: Particle, def: [0., 0., 0.] }, + { name: "partMaxSpeed", old: "instMaxVelocity", group: Particle, def: [0.] }, + { name: "partDampen", old: "instDampen", group: Particle, def: [0.] }, + { name: "partScale", old: "instScale", group: Particle, def: [1.] }, + { name: "scaleMultiplier", old: "instScaleOverTime", group: OverTime, def: [1.] }, + { name: "partStretch", old: "instStretch", group: Particle, def: [1., 1., 1.] }, + { name: "partStretchVelocity", old: "instStretchVelocity", group: Particle, def: [0.] }, + { name: "partRotation", old: "instRotation", group: Particle, def: [0., 0., 0.] }, + { name: "partPivot", old: "instPivot", group: Particle, def: [0., 0., 0.] }, + { name: "partOffset", old: "instOffset", group: Particle, def: [0., 0., 0.] }, + ]; + + static var EMITTER_2D : Array = [ + { name: "lifeTime", old: "lifeTime", group: Spawn, def: [1.] }, + { name: "partScale", old: "instScale", group: Particle, def: [1.] }, + { name: "scaleMultiplier", old: "instScaleOverTime", group: OverTime, def: [1.] }, + { name: "partStretch", old: "instStretch", group: Particle, def: [1., 1.] }, + { name: "partRotation", old: "instRotation", group: Particle, def: [0.] }, + { name: "partOffset", old: "instOffset", group: Particle, def: [0., 0.] }, + { name: "partAcceleration", old: "instAcceleration", group: Particle, def: [0., 0.] }, + { name: "partWorldAcceleration", old: "instWorldAcceleration", group: Particle, def: [0., 0.] }, + { name: "partSpeed", old: "instSpeed", group: Particle, def: [0., 0.] }, + { name: "partWorldSpeed", old: "instWorldSpeed", group: Particle, def: [0., 0.] }, + { name: "startSpeed", old: "instStartSpeed", group: Spawn, def: [0., 0.] }, + { name: "startWorldSpeed", old: "instStartWorldSpeed", group: Spawn, def: [0., 0.] }, + { name: "partOrbitSpeed", old: "instOrbitSpeed", group: Particle, def: [0.] }, + { name: "orbitSpeedMultiplier", old: "instOrbitSpeedOverTime", group: OverTime, def: [1.] }, + { name: "partMaxSpeed", old: "instMaxVelocity", group: Particle, def: [0.] }, + { name: "partDampen", old: "instDampen", group: Particle, def: [0.] }, + ]; + + /** The emitter properties animated over the emitter time, with their default **/ + static var PROPERTIES_3D = [{ name: "emitRate", def: 5. }, { name: "emitRateMin", def: 5. }, { name: "emitRateMax", def: 5. }, + { name: "burstParticleCount", def: 1. }, { name: "emitAngle", def: 30. }]; + static var PROPERTIES_2D = [{ name: "burstParticleCount", def: 1. }, { name: "emitRate", def: 5. }, { name: "emitRateMin", def: 5. }, + { name: "emitRateMax", def: 5. }, { name: "emitRadius", def: 20. }, { name: "emitAngle1", def: 20. }, { name: "emitAngle2", def: 40. }, + { name: "emitWidth", def: 1. }, { name: "emitHeight", def: 1. }]; + + /** The properties saved under another name, their curves renamed with them **/ + static var PROPERTY_RENAMES = ["seedGroup" => "seed", "speedFactor" => "timeScale", "burstDelay" => "burstInterval", + "emitRateChangeDelay" => "emitRateChangeTime", "emitAngle1" => "emitStartAngle", "emitAngle2" => "emitEndAngle", + "emitDomeCut" => "emitDomeHeight", "animationSpeed" => "animationCycles"]; + + /** The old params added to their curve instead of being multiplied by it **/ + static var ADDITIVE_3D = ["Rotation", "Offset", "Pivot"]; + static var ADDITIVE_2D = ["Rotation", "Offset"]; + + static function legacyRand( old : String ) { + return old == "lifeTime" ? "lifeTimeRand" : old + "_rand"; + } + + function emitterPass( n : RawNode ) { + if( Migrate.isEmitter(n.data) ) + upgradeEmitter(n); + for( c in n.children.copy() ) + emitterPass(c); + } + + function upgradeEmitter( e : RawNode ) { + var is2D = e.isA(hrt.prefab.fx.Emitter2D); + var defs = is2D ? EMITTER_2D : EMITTER_3D; + var props : Dynamic = get(e.data, "props") ?? {}; + var params = upgradeParams(e, props, defs, is2D ? ADDITIVE_2D : ADDITIVE_3D); + var properties = [for( s in (is2D ? PROPERTIES_2D : PROPERTIES_3D) ) { name: s.name, value: num(get(props, s.name), s.def) }]; + upgradeEmitterAnims(e, properties, defs, params); + + // orbit speeds in degrees per second + var orbit = params.get("partOrbitSpeed"); + if( orbit != null ) { + var k = 180.0 / Math.PI; + params.set("partOrbitSpeed", [for( m in orbit ) switch( m ) { + case null: null; + case MConstant(v): MConstant(v * k); + case MRange(a, b): MRange(a * k, b * k); + case MCurve(s, f, t): MCurve(s, f * k, t * k); + case MRandomCurve(s, f0, f1, t0, t1): MRandomCurve(s, f0 * k, f1 * k, t0 * k, t1 * k); + case MCurveRange(s, af, at, bf, bt): MCurveRange(s, af * k, at * k, bf * k, bt * k); + }]); + change(e, "orbit speed in degrees per second"); + } + + var renames = []; + for( d in defs ) { + var v = params.get(d.name); + if( v == null ) + continue; + params.set(d.name, [for( m in v ) bakeModeCurves(e, m)]); + // the curve of a particle param is the one named after it + for( i => m in v ) { + var path = switch( m ) { case MCurve(s, _), MRandomCurve(s, _), MCurveRange(s, _): s; case null, _: null; }; + var n = path == null ? null : locate(path, e); + if( n != null ) + renames.push({ node: n, name: d.name + (d.def.length == 1 ? "" : ":" + COMPONENTS[i]) }); + } + } + // the curve of a renamed property, the one the old runtime read + for( f => name in PROPERTY_RENAMES ) { + var n = Lambda.find(e.children, c -> c.isCurve && c.enabled() && sameName(c.name, f)); + if( n != null ) + renames.push({ node: n, name: name }); + } + renameCurves(renames); + + // the sort and the random color are a mode each + if( Reflect.field(props, "enableSort") == false ) { + props.sortMode = "None"; + change(e, "enableSort off is sortMode None"); + } + // the 2D emitter took the gradient without useRandomColor + var rc = Reflect.field(props, "useRandomColor") == true, rg = Reflect.field(props, "useRandomGradient") == true; + if( rc || (rg && is2D) ) { + props.randomColorMode = rg ? "Gradient" : "Colors"; + change(e, "random color mode"); + } + for( f in ["enableSort", "useRandomColor", "useRandomGradient"] ) + Reflect.deleteField(props, f); + // the colors are ARGB ints, opaque: their alpha was never read + for( f in (is2D ? ["color", "randomColor1", "randomColor2"] : ["randomColor1", "randomColor2"]) ) { + var c : Dynamic = Reflect.field(props, f); + if( Std.isOfType(c, Array) ) { + var v = new h3d.Vector(num(c[0], 0), num(c[1], 0), num(c[2], 0)); + Reflect.setField(props, f, v.toColor()); + change(e, "color as an int"); + } + } + // a sphere this small emits from its center: a Point (the emitter scale scales the shape) + var small = Lambda.foreach(["scaleX", "scaleY", "scaleZ"], f -> Math.abs(num(fieldOf(e, f), 1)) < 0.05); + if( !is2D && (get(props, "emitShape") ?? "Sphere") == "Sphere" && small && group(e, "scale").length == 0 ) { + props.emitShape = "Point"; + Reflect.deleteField(props, "emitSurface"); + change(e, "small sphere is a Point"); + } + upgradeEmission(e, props); + if( !is2D ) + upgradeParticleFade(e, props); + + // the properties and the params with modes are fields of the emitter + var old = new Map(); + for( d in defs ) { + old.set(d.name, true); + old.set(d.old, true); + old.set(legacyRand(d.old), true); + } + for( f in Reflect.fields(props) ) + if( !old.exists(f) ) + RawNode.setField(e.data, PROPERTY_RENAMES.get(f) ?? f, Reflect.field(props, f)); + Reflect.deleteField(e.data, "props"); + for( d in defs ) { + var v = params.get(d.name); + var j = v == null ? null : saveValue(v); + if( j != null ) + RawNode.setField(e.data, d.name, j); + } + change(e, "emitter params with modes"); + + // the shaders on its particles + if( !is2D ) + for( s in particleShaders(e) ) + upgradeParticleShader(s); + } + + /** + A `ParticleFade` on the particles of an emitter is its Fade In / Fade Out (the same linear ramps), when it is the only + one the emitter applies, static, with a power of 1 (the emitter fade has none), not under a disabled or editor only + prefab: the shader is removed. The others stay. + **/ + function upgradeParticleFade( e : RawNode, props : Dynamic ) { + // the runtime applies the enabled ones (`EmitterObject.init`) + var fades = [for( s in particleShaders(e) ) if( s.enabled() && get(s.data, "source") == "hrt.shader.ParticleFade" ) s]; + if( fades.length != 1 ) + return; + var s = fades[0]; + var p = s; + while( p != e ) { + if( !p.enabled() || get(p.data, "editorOnly") == true ) + return; + p = p.parent; + } + if( Lambda.exists(s.children, c -> c.isCurve && c.enabled()) ) + return; + var sp : Dynamic = get(s.data, "props"); + inline function value( name : String, def : Float ) return num(get(sp, propName(sp, name.toLowerCase()) ?? name), def); + if( Math.abs(value("power", 1) - 1) > 1e-3 ) + return; + // the defaults of the shader + var fadeIn = value("fadeInLife", 0.1), fadeOut = value("fadeOutLife", 0.1); + if( fadeIn != 0 ) props.fadeIn = fadeIn; + if( fadeOut != 0 ) props.fadeOut = fadeOut; + s.parent.children.remove(s); + (get(s.parent.data, "children") : Array).remove(s.data); + change(e, "ParticleFade is the emitter fade in / out"); + } + + /** + The rate and the particles per burst are Emission params: their curve, baked with their value (`upgradeEmitterAnims`), makes + them a Curve. The random rate (`InfinityRandom`, between `emitRateMin` and `emitRateMax`) is a Range of the rate, a Curve Range + when a bound has a curve (its two datas the curves of the bounds, a flat one for a bound without). The rates the emit type did + not read are dropped, with their curves. + **/ + function upgradeEmission( e : RawNode, props : Dynamic ) { + var random = get(props, "emitType") == "InfinityRandom"; + function curves( name : String ) return [for( c in e.children ) if( c.isCurve && sameName(c.name, name) ) c]; + function curve( name : String ) return Lambda.find(curves(name), c -> c.enabled()); + var dataChildren : Array = get(e.data, "children"); + function remove( c : RawNode ) { + e.children.remove(c); + dataChildren.remove(c.data); + } + for( name in (random ? ["emitRate"] : ["emitRateMin", "emitRateMax"]) ) { + Reflect.deleteField(props, name); + for( c in curves(name) ) { + remove(c); + warn(c, 'curve of $name, which its emit type did not read, removed'); + } + } + for( name in ["emitRate", "burstParticleCount"] ) { + var c = curve(name); + if( c != null ) { + Reflect.setField(props, name, obj(["mode", "Curve"])); + change(c, "emission curve, a Curve"); + } + } + if( !random ) + return; + var min = num(get(props, "emitRateMin"), 5), max = num(get(props, "emitRateMax"), 5); + var cmin = curve("emitRateMin"), cmax = curve("emitRateMax"); + for( f in ["emitType", "emitRateMin", "emitRateMax"] ) + Reflect.deleteField(props, f); + if( cmin == null && cmax == null ) { + props.emitRate = min == max ? min : obj(["mode", "Range", "min", min, "max", max]); + change(e, "random rate, a Range of the rate"); + return; + } + function bound( c : RawNode, v : Float ) : Dynamic { + if( c == null ) + return obj(["keys", [obj(["time", 0., "value", v, "mode", 2])]]); + if( blendMode(c) != 0 || get(c.data, "remapPath") != null ) + warn(c, "bound of the random rate on a curve that is not a plain one, its own keys taken"); + remove(c); + return datum(c.data); + } + var range = newCurve(freeName(e, "emitRate")); + Reflect.deleteField(range, "keys"); + range.datas = [bound(cmin, min), bound(cmax, max)]; + e.addChild(range); + props.emitRate = obj(["mode", "CurveRange"]); + change(e, "random rate with curves, a Curve Range of the rate"); + } + + /** + The curves of the Curve and CurveRange modes hold their values: `lerp(from, to, curve)` is baked into them. The curve of a + RandomCurve gets the middle of its bounds `m(c)`; their half width `w(c)` (both `lerp(from, to, c)` before) is its random + spread, which follows the value of the curve (`EmitterParam.spreadLine`): `w` at the lowest and the highest value of the + baked curve. Exact, the half width being affine in the curve as its middle is, but when the middle is flat and the width + is not (`(k + a * random) * c` with `k = 0`). The random blend of a CurveRange then becomes a curve of two datas + (`mergeRandomBlend`). + **/ + function bakeModeCurves( owner : RawNode, m : Mode ) : Mode { + function bake( n : RawNode, from : Float, to : Float ) { + if( n == null || (from == 0 && to == 1) ) + return; + if( bakedModeCurves.exists(n) ) + warn(n, "curve of several params, baked with the bounds of the first one"); + else + bakeLinear(n, to - from, from); + bakedModeCurves.set(n, true); + } + return switch( m ) { + case MCurve(s, from, to): + bake(locate(s, owner), from, to); + MCurve(s, 0, 1); + case MRandomCurve(s, f0, f1, t0, t1): + var n = locate(s, owner); + var fm = (f0 + f1) * 0.5, tm = (t0 + t1) * 0.5, wf = (f1 - f0) * 0.5, wt = (t1 - t0) * 0.5; + // a width following a curve the spread cannot follow, around a constant value or along a flat curve: a Curve Range, + // its bounds `lerp(f, t, c)` as its two datas + var r0 = n == null || wf == wt ? null : valueRange(n); + var flat = r0 != null && r0.max - r0.min <= 1e-9 * Math.max(1, Math.abs(r0.max)); + if( n != null && wf != wt && (tm == fm || flat) && blendMode(n) == 0 && !bakedModeCurves.exists(n) ) { + splitRange(n, f0, t0, f1, t1, "random amount following a curve around a constant value, a curve of two datas"); + bakedModeCurves.set(n, true); + return MCurveRange(s, 0, 1, 0, 1); + } + bake(n, fm, tm); + var low = wf, high = wf; + if( wf != wt ) { + var r = n == null ? null : valueRange(n); + if( r == null || tm == fm ) { + if( n != null ) + warn(n, tm == fm ? "random amount following a curve around a constant value, approximated by its largest amount" + : "random amount following a curve that is not a plain one or a blend, approximated by its largest amount"); + low = high = Math.abs(wf) > Math.abs(wt) ? wf : wt; + } else { + // the half width at a value v of the baked curve: `lerp(wf, wt, c)` with v = `lerp(fm, tm, c)` + inline function width( v : Float ) return wf + (wt - wf) * (v - fm) / (tm - fm); + low = width(r.min); + high = width(r.max); + } + } + MRandomCurve(s, -low, low, -high, high); + case MCurveRange(s, af, at, bf, bt): + var n = locate(s, owner); + if( n != null && !mergedBlends.exists(n) ) { + bake(n.children[0], af, at); + bake(n.children[1], bf, bt); + mergeRandomBlend(n); + } else if( n != null && (af != 0 || at != 1 || bf != 0 || bt != 1) ) + warn(n, "random blend of several params, baked with the bounds of the first one"); + MCurveRange(s, 0, 1, 0, 1); + case null, _: m; + } + } + + /** The plain curve `n` becomes a curve of two datas, its keys remapped from 0..1 to `a0`..`a1` and to `b0`..`b1` (a Curve Range) **/ + function splitRange( n : RawNode, a0 : Float, a1 : Float, b0 : Float, b1 : Float, why : String ) { + var d = n.data; + var keys : Array = get(d, "keys"); + if( keys == null || keys.length == 0 ) { + // the default keys of `Curve.load` + var mode = get(d, "keyMode") ?? 2; + keys = [obj(["time", 0., "value", 0., "mode", mode]), obj(["time", 1., "value", 1., "mode", mode])]; + } + function datum( from : Float, to : Float ) : Dynamic { + var ks : Array = deepCopy(keys); + for( k in ks ) { + // the mode of the curve, which the datas do not have + if( get(k, "mode") == null ) k.mode = get(d, "keyMode") ?? 2; + k.value = from + (to - from) * num(get(k, "value"), CurveKeyDefault); + for( h in [get(k, "prevHandle"), get(k, "nextHandle")] ) + if( h != null ) + h.dv = (to - from) * num(get(h, "dv"), CurveKeyDefault); + } + var o : Dynamic = obj(["keys", ks]); + if( get(d, "loop") == true ) o.loop = true; + return o; + } + var datas = [datum(a0, a1), datum(b0, b1)]; + for( f in ["keys", "keyMode", "loop", "previewKeys"] ) + Reflect.deleteField(d, f); + RawNode.setField(d, "datas", datas); + change(n, why); + } + + /** + The lowest and highest values of a curve as the runtime reads them (`hrt.impl.Curve.getValueRange`): a plain curve, or a + blend of its two child curves (both); null for another curve + **/ + static function valueRange( n : RawNode ) : { min : Float, max : Float } { + switch( blendMode(n) ) { + case 0: + var keys : Array = get(n.data, "keys"); + if( keys == null || keys.length == 0 ) + return { min: 0., max: 1. }; + var c = new hrt.impl.Curve(); + @:privateAccess c.copyFromDynamic({ keys: deepCopy(keys) }); + return c.getValueRange(); + case 1: + var cs = [for( i in 0...hxd.Math.imin(2, n.children.length) ) if( n.children[i].isCurve ) valueRange(n.children[i])]; + if( cs.length == 0 || cs.indexOf(null) >= 0 ) + return null; + var r = cs[0]; + for( c in cs ) r = { min: Math.min(r.min, c.min), max: Math.max(r.max, c.max) }; + return r; + default: + return null; + } + } + + /** + A Curve Range reads the two datas of one curve (`ParamBuilder.comp`): the random blend `n` gets the keys of its two child + curves (`lerp(child0(t), child1(t), random)` before) as its datas, and loses its children. + **/ + function mergeRandomBlend( n : RawNode ) { + mergedBlends.set(n, true); + var sub = [for( i in 0...2 ) i < n.children.length && n.children[i].isCurve ? n.children[i] : null]; + if( sub[0] == null || sub[1] == null ) { + fail(n, "random blend without its two child curves"); + return; + } + // the old runtime read the own keys of a random blend through a reference: they are lost + if( users.exists(n) && users.get(n).length > 0 ) + warn(n, "random blend read by references, they read its first child curve now"); + var d = n.data; + var datas = [datum(sub[0].data), datum(sub[1].data)]; + for( f in ["blendMode", "blendParam", "remapPath", "scale", "offset", "keys", "keyMode", "loop", "previewKeys", "children"] ) + Reflect.deleteField(d, f); + RawNode.setField(d, "datas", datas); + n.rebuildChildren(); + change(n, "random blend, a curve of two datas"); + } + + /** The keys of the plain curve `c` (its data) as a data of a curve (`keys`, `loop`) **/ + static function datum( c : Dynamic ) : Dynamic { + var keys : Array = get(c, "keys"); + if( keys == null || keys.length == 0 ) { + // the default keys of `Curve.load` + var mode = get(c, "keyMode") ?? 2; + keys = [obj(["time", 0., "value", 0., "mode", mode]), obj(["time", 1., "value", 1., "mode", mode])]; + } + var d : Dynamic = obj(["keys", deepCopy(keys)]); + if( get(c, "loop") == true ) d.loop = true; + return d; + } + + /** The random blend `n` becomes a plain curve with the keys of `from`, one of its child curves, or its own **/ + function flattenBlend( n : RawNode, from : RawNode ) { + var keys = datum(from.data), d = n.data; + for( f in ["blendMode", "blendParam", "keys", "keyMode", "loop", "children"] ) + Reflect.deleteField(d, f); + RawNode.setField(d, "keys", keys.keys); + if( keys.loop == true ) RawNode.setField(d, "loop", true); + n.rebuildChildren(); + } + + /** + The runtime read a random blend as a random between its two child curves for the params of the emitters only + (`upgradeParams`, `upgradeEmitterAnims`), else as a plain curve, its own keys: what they become. + **/ + function randomBlendPass( n : RawNode ) { + if( n.isCurve && blendMode(n) == 2 ) { + flattenBlend(n, n); + change(n, "random blend out of the emitter params, its own keys"); + } + for( c in n.children ) + randomBlendPass(c); + } + + /** The shaders the particles of `e` get (`EmitterHelper.getParticleEmitter`) **/ + static function particleShaders( e : RawNode, ?out : Array ) : Array { + if( out == null ) out = []; + for( c in e.children ) { + if( c.isA(hrt.prefab.fx.Emitter) || c.isA(hrt.prefab.l3d.Trails) ) continue; + if( c.isA(hrt.prefab.Shader) ) out.push(c); + particleShaders(c, out); + } + return out; + } + + static function isUnset( v : Array ) { + for( m in v ) if( m != null ) return false; + return true; + } + + /** + The old params of an emitter in their modes: the base value `k`, the random amount `a` + (`_rand`, the value is `k + a * random(-1, 1)`), and curves found by name among the children + (`instScale`, `instStretch:x`, `instScale:rand`...), first enabled match, case insensitive. A + curve multiplied the value, or was added to it for the rotation, offset and pivot; a curve in + RandomBlend mode replaced it by a random between its two child curves. + **/ + function upgradeParams( e : RawNode, props : Dynamic, defs : Array, additive : Array ) : Map> { + var params = new Map>(); + // the life time was already a plain value + var life : Dynamic = get(props, "lifeTime"); + if( Std.isOfType(life, Float) || Std.isOfType(life, Int) ) + params.set("lifeTime", [MConstant(life)]); + inline function findCurve( name : String ) : RawNode { + return Lambda.find(e.children, c -> c.isCurve && c.enabled() && sameName(c.name, name)); + } + for( d in defs ) { + var old = d.old; + var baseField : Dynamic = old != d.name ? get(props, old) : null; + var randField : Dynamic = get(props, legacyRand(old)); + var dim = d.def.length; + var cur = params.get(d.name); + var out : Array = [for( i in 0...dim ) cur == null ? null : cur[i]]; + var changed = false; + for( i in 0...dim ) { + var suffix = dim == 1 ? "" : ":" + COMPONENTS[i]; + // the old code reads the arrays as Float, a missing element is 0 + inline function comp( v : Dynamic ) : Null { + return v == null ? null : dim == 1 ? (Std.isOfType(v, Array) ? null : num(v, 0)) : (Std.isOfType(v, Array) ? num((v : Array)[i], 0) : null); + } + var base = comp(baseField); + if( d.name == "lifeTime" && out[i] != null ) + base = switch( out[i] ) { case MConstant(v): v; default: base; }; + var rand = comp(randField); + var hasRand = rand != null && rand != 0; + var k = base ?? d.def[i]; + var ci = findCurve(old + suffix); + var ri = findCurve(old + suffix + ":rand"); + var isBlend = ci != null && blendMode(ci) == 2; + var mode : Mode = null; + if( d.group != Particle ) { + // the curves of these params are animation curves over the emitter time (`upgradeEmitterAnims`) + if( ri != null ) + warn(e, '${old + suffix}:rand: a random curve on a ${d.group == Spawn ? "Spawn" : "over time"} param has no equivalent, ignored'); + if( hasRand && d.group == Spawn ) + mode = MRange(k - rand, k + rand); + else { + if( hasRand ) + warn(e, '${d.name}: random amount $rand ignored, an over time setting is a constant'); + if( base != null || hasRand ) + mode = MConstant(k); + } + } else { + var isAdditive = Lambda.exists(additive, a -> old.indexOf(a) >= 0); + var curve = ci?.uniqueName(); + if( ri != null && ci == null ) { + // the random amount follows a curve: k + a * random * curve(t) + var a = rand ?? 1.0; + mode = MRandomCurve(ri.uniqueName(), k, k, k - a, k + a); + } else { + if( ri != null ) + warn(e, '${old + suffix}:rand: a random curve next to a curve has no equivalent, ignored'); + if( isBlend ) { + // lerp(curve0(t), curve1(t), random), the base and random amount are not used + var sub = ci.children; + if( sub.length < 2 || !sub[0].isCurve || !sub[1].isCurve ) + warn(e, '${old + suffix}: random blend without its two child curves'); + else if( !sub[0].enabled() || !sub[1].enabled() ) + warn(e, '${old + suffix}: disabled child curve of a random blend'); + mode = MCurveRange(ci.uniqueName(), 0, 1, 0, 1); + } else if( curve != null ) { + if( isAdditive ) + mode = hasRand ? MRandomCurve(curve, k - rand, k + rand, k - rand + 1, k + rand + 1) : MCurve(curve, k, k + 1); + else + mode = hasRand ? MRandomCurve(curve, 0, 0, k - rand, k + rand) : MCurve(curve, 0, k); + } else if( hasRand ) + mode = MRange(k - rand, k + rand); + else if( base != null ) + mode = MConstant(k); + } + } + if( mode != null || baseField != null || randField != null ) { + out[i] = mode; + changed = true; + } + } + if( changed ) { + if( isUnset(out) ) + params.remove(d.name); + else + params.set(d.name, out); + } + } + return params; + } + + /** + The old animation curves of an emitter: the settings (`settings`, with their value), the spawn + params and the over time settings were multiplied by their curve, found by name (the old names). The + curves are baked and named after what they animate (`EmitterParam.curveKey`). A range + `(k ± a) * curve` gets one curve per bound (the second one a reference to the first). + **/ + function upgradeEmitterAnims( e : RawNode, settings : Array<{ name : String, value : Float }>, defs : Array, + params : Map> ) { + inline function find( name : String ) : RawNode { + return Lambda.find(e.children, c -> c.isCurve && c.enabled() && sameName(c.name, name)); + } + inline function isBlend( n : RawNode ) return blendMode(n) == 2; + var renames = []; + + // a random blend on a setting read the random of the first particle: its first child curve + function settingBlend( n : RawNode ) { + var sub = n.curveChildren(); + if( sub.length == 0 ) { + fail(n, "random blend without its two child curves"); + return; + } + warn(n, "random blend on an emitter setting, its first curve"); + flattenBlend(n, sub[0]); + } + + // emitter settings: value * curve + for( s in settings ) { + var n = find(s.name); + if( n == null || !isLegacy(n) ) continue; + if( isBlend(n) ) + settingBlend(n); + else + bakeLinear(n, s.value, 0); + change(n, "emitter setting"); + } + + // spawn params and over time settings + for( d in defs ) { + if( d.group == Particle || d.name == "lifeTime" ) continue; + var dim = d.def.length; + var value = params.get(d.name); + var out : Array = [for( i in 0...dim ) value == null ? null : value[i]]; + var changed = false; + for( i in 0...dim ) { + var suffix = dim == 1 ? "" : ":" + COMPONENTS[i]; + var key = d.name + suffix; + var m = out[i]; + var bounds = switch( m ) { case MRange(lo, hi): { lo: lo, hi: hi }; case null, _: null; }; + var k = switch( m ) { case MConstant(v): v; case null, _: d.def[i]; }; + var c = find(d.old + suffix); + if( c == null || !isLegacy(c) ) continue; + if( d.group == OverTime ) { + renames.push({ node: c, name: key }); + if( isBlend(c) ) + settingBlend(c); + else + bakeLinear(c, k, 0); + change(c, "over time"); + continue; + } + // the curve is the one of the param mode, named after it (`bakeModeCurves`) + changed = true; + if( isBlend(c) ) { + // random between the two child curves, the value is not used: a Curve Range, its two datas the child curves + out[i] = MCurveRange(c.uniqueName(), 0, 1, 0, 1); + change(c, "spawn, random blend: a curve range"); + } else if( bounds == null ) { + out[i] = MCurve(c.uniqueName(), 0, k); + change(c, "spawn value"); + } else { + // (k ± a) * c: a RandomCurve, as the particle params; a reference becomes a copy of its curve, for the range of its values + var depth = 0; + while( blendMode(c) == 3 && depth++ < 16 ) + dereference(c); + out[i] = MRandomCurve(c.uniqueName(), 0, 0, bounds.lo, bounds.hi); + change(c, "spawn range, a random curve"); + } + } + if( changed ) { + if( isUnset(out) ) + params.remove(d.name); + else + params.set(d.name, out); + } + } + renameCurves(renames); + markAll(e, e, false); + } + + /** + Renames curves, the paths of the file to them or through them follow. The other curves of their old name (disabled, or after an + enabled one) were not read: they keep it, their values are not upgraded. + **/ + function renameCurves( renames : Array<{ node : RawNode, name : String }> ) { + if( renames.length == 0 ) + return; + var refs = []; + function collect( n : RawNode ) { + if( n.isCurve ) + for( f in (blendMode(n) == 3 ? ["blendParam", "remapPath"] : ["remapPath"]) ) { + var v : Dynamic = get(n.data, f); + var target = Std.isOfType(v, String) ? locate(v) : null; + if( target != null ) + refs.push({ node: n, field: f, target: target }); + } + for( c in n.children ) + collect(c); + } + collect(root); + for( r in renames ) { + if( Lambda.exists(r.node.parent.children, c -> c != r.node && c.enabled() && sameName(c.name, r.name)) ) + warn(r.node, 'renamed ${r.name}, the name of another curve'); + r.node.rename(r.name); + } + for( r in refs ) { + var path = pathOf(r.target); + if( path != null && path != get(r.node.data, r.field) ) { + Reflect.setField(r.node.data, r.field, path); + change(r.node, r.field + " path to a renamed curve"); + } + } + } + + /** + The curves of a shader under an emitter, evaluated over the particle life. The shader definition + is not known, the kind of a param comes from its stored value: a float was `curve * value`, a + vector came from its component curves alone, 0 for the others (1 for w). Colors keep their curves. + **/ + function upgradeParticleShader( s : RawNode ) { + var props : Dynamic = get(s.data, "props"); + var out = new Map>(); + var seen = new Map(); + for( c in s.children ) { + if( !c.enabled() || !c.isCurve ) continue; + var prefix = c.name.split(":")[0]; + var low = prefix.toLowerCase(); + if( seen.exists(low) ) continue; + seen.set(low, true); + // the runtime finds the curves from the shader var name, case insensitive + var name = propName(props, low) ?? prefix; + var prop : Dynamic = shaderValue(s, name); + var group = [for( x in s.children ) if( x.enabled() && x.isCurve && sameName(x.name.split(":")[0], name) ) x]; + var components = Lambda.exists(group, x -> x.name.indexOf(":") >= 0); + var isVector = Std.isOfType(prop, Array) || (prop == null && components); + if( isVector && low.indexOf("color") >= 0 ) + continue; + if( isVector ) { + var dim = Std.isOfType(prop, Array) ? hxd.Math.imin((prop : Array).length, 4) : 4; + var v : Array = []; + // a component without curve was 0 (1 for w): its prop + var base : Array = [for( k in 0...dim ) Std.isOfType(prop, Array) ? (prop : Array)[k] : 0.]; + for( k in 0...dim ) { + var suffix = ":" + COMPONENTS[k]; + var x = Lambda.find(group, x -> StringTools.endsWith(x.name.toLowerCase(), suffix)); + v.push(x != null ? MCurve(x.uniqueName(), 0, 1) : null); + if( x == null ) base[k] = k == 3 ? 1. : 0.; + } + if( props == null ) { + props = {}; + RawNode.setField(s.data, "props", props); + } + Reflect.setField(props, name, base); + out.set(name, v); + } else { + var x = Lambda.find(s.children, x -> x.isCurve && x.enabled() && sameName(x.name, name)); + // component curves on a float are not read + if( x == null ) + continue; + // the old code multiplied by the value when it is a number, by 1 otherwise + if( prop == null ) + unknownBase(x, name); + out.set(name, [MCurve(x.uniqueName(), 0, isNumber(prop) ? num(prop, 0) : 1.0)]); + } + } + if( !out.iterator().hasNext() ) + return; + // the modes of the params by name, their curves read over the particle life named after their param and component (`EmitterParam.curveName`) + var names = [for( name in out.keys() ) name]; + names.sort(Reflect.compare); + var params : Dynamic = {}, renames = []; + for( name in names ) { + var v = out.get(name); + var modes = [for( i => m in v ) + switch( bakeModeCurves(s, m) ) { + case m = MCurve(path, _): + var key = v.length == 1 ? name : name + ":" + COMPONENTS[i]; + var n = locate(path, s); + if( n != null && n.name != key ) + renames.push({ node: n, name: key }); + m; + case null, _: null; + }]; + var saved = saveValue(modes); + if( saved != null ) + Reflect.setField(params, name, saved); + } + renameCurves(renames); + RawNode.setField(s.data, "particleParams", params); + change(s, "particle shader params with modes"); + } + + // Format 1 of the modes, `EmitterParam.saveMode`: a constant is a plain number, the other modes an object with a `mode` field, a vector an object of its components. + + static function saveValue( v : Array ) : Dynamic { + if( v == null || isUnset(v) ) + return null; + if( v.length == 1 ) + return saveMode(v[0]); + var o : Dynamic = {}; + for( i => m in v ) + if( m != null ) + Reflect.setField(o, COMPONENTS[i], saveMode(m)); + return o; + } + + /** The bounds of MCurve, MRandomCurve and MCurveRange are baked (`bakeModeCurves`), their curve is the one named after their param **/ + static function saveMode( m : Mode ) : Dynamic { + return switch( m ) { + case null: null; + case MConstant(v): v; + case MRange(min, max): obj(["mode", "Range", "min", min, "max", max]); + case MCurve(_): obj(["mode", "Curve"]); + case MRandomCurve(_, f0, f1, t0, t1): + var o : Dynamic = obj(["mode", "RandomCurve"]); + // an amount negligible next to the other one is 0: the overshoot of the curve beyond its keys + var low = (f1 - f0) * 0.5, high = (t1 - t0) * 0.5; + if( Math.abs(low) < 1e-3 * Math.abs(high) ) low = 0; + if( Math.abs(high) < 1e-3 * Math.abs(low) ) high = 0; + if( low != 0 ) o.spreadMin = low; + if( high != 0 ) o.spreadMax = high; + o; + case MCurveRange(_): obj(["mode", "CurveRange"]); + } + } + + // ---------------------------------------------------------------------------------------- + // References + + function referencePass( n : RawNode ) { + if( n.isA(hrt.prefab.Reference) ) + upgradeReference(n); + for( c in n.children ) + referencePass(c); + } + + /** + The overrides of a reference were made for the file it references in version 0: they are + applied on that file, the result upgraded, and the overrides made again from the upgraded + file to it. Kept as they are when they still give the same result. + **/ + function upgradeReference( n : RawNode ) { + var source : String = get(n.data, "source"); + var overrides : Dynamic = get(n.data, "overrides"); + if( source == null || overrides == null ) + return; + var raw = ctx.readRaw(source); + if( raw == null ) { + warn(n, 'overrides of $source, which cannot be read: kept as they are'); + return; + } + // the file is already migrated (a project migrated in part), or has nothing to migrate + if( Migrate.versionOf(raw) != 0 ) + return; + var upgradedSource = ctx.migrated(source); + if( upgradedSource == null ) { + warn(n, 'overrides of $source, which cannot be upgraded: kept as they are'); + return; + } + if( haxe.Json.stringify(upgradedSource) == haxe.Json.stringify(raw) ) + return; + var merged = hrt.prefab.Diff.apply(deepCopy(raw), deepCopy(overrides)); + var target = ctx.upgradeAs(merged, source); + var same = hrt.prefab.Diff.apply(deepCopy(upgradedSource), deepCopy(overrides)); + if( haxe.Json.stringify(same) == haxe.Json.stringify(target) ) + return; + switch( hrt.prefab.Diff.diffPrefab(upgradedSource, target) ) { + case Skip: + Reflect.deleteField(n.data, "overrides"); + case Set(d): + Reflect.setField(n.data, "overrides", d); + } + change(n, 'overrides of $source made for its upgrade'); + } +} + +#end diff --git a/hide_common.hxml b/hide_common.hxml index 0a557df58..1e5a1bb85 100644 --- a/hide_common.hxml +++ b/hide_common.hxml @@ -1,6 +1,6 @@ common.hxml -D hide - +-D fx-migration --macro include("h3d.shader") --macro include("h3d.anim") --macro include("h3d.prim") diff --git a/hide_js/hide/comp/SVG.hx b/hide_js/hide/comp/SVG.hx index 7b50c5ca5..e8cf65dfa 100644 --- a/hide_js/hide/comp/SVG.hx +++ b/hide_js/hide/comp/SVG.hx @@ -41,6 +41,14 @@ class SVG extends Component { return make(parent, "rect", {x:x, y:y, width:width, height:height}, style); } + public function rectBetween(?parent: Element, x0:Float, y0:Float, x1:Float, y1:Float, ?style:Dynamic) { + return rect(parent, Math.min(x0, x1), Math.min(y0, y1), Math.abs(x1 - x0), Math.abs(y1 - y0), style); + } + + public function square(?parent: Element, x:Float, y:Float, size:Float, ?style:Dynamic) { + return rect(parent, x - size / 2, y - size / 2, size, size, style); + } + public function line(?parent: Element, x1:Float, y1:Float, x2:Float, y2:Float, ?style:Dynamic) { return make(parent, "line", {x1:x1, y1:y1, x2:x2, y2:y2}, style); } diff --git a/hide_js/hide/comp/CurveEditor.hx b/hide_js/hide/comp/Timeline.hx similarity index 83% rename from hide_js/hide/comp/CurveEditor.hx rename to hide_js/hide/comp/Timeline.hx index d0bbc8b40..0b44e9e7b 100644 --- a/hide_js/hide/comp/CurveEditor.hx +++ b/hide_js/hide/comp/Timeline.hx @@ -1,8 +1,27 @@ package hide.comp; -import hrt.prefab.Curve; +import hrt.prefab.fx.Curve.CurveBlendMode; -typedef CurveKey = hrt.prefab.Curve.CurveKey; +typedef CurveKey = hrt.impl.Curve.CurveKey; + +typedef EditedCurve = { + var name : String; + var data(get, never) : hrt.impl.Curve; +} + +/** A data of a curve of several (a blend, a range), edited as a curve of its own **/ +class CurveData { + public var name : String; + public var data(get, never) : hrt.impl.Curve; + var curve : hrt.prefab.fx.Curve; + var index : Int; + public function new(curve: hrt.prefab.fx.Curve, index: Int) { + this.curve = curve; + this.index = index; + name = curve.name + " " + index; + } + function get_data() return curve.datas[index]; +} interface CurveEditorComponent { function refresh(?anim: Bool = false): Void; @@ -15,14 +34,14 @@ interface CurveEditorComponent { class EventsEditor extends Component implements CurveEditorComponent { public var fxEditor : hide.view.FXEditor; - public var curveEditor : CurveEditor; + public var curveEditor : Timeline; public var events : Array = []; var lastValue : Array; var svg : SVG; var eventGroup: Element; - public function new(?parent, fxEditor: hide.view.FXEditor, curveEditor: CurveEditor) { + public function new(?parent, fxEditor: hide.view.FXEditor, curveEditor: Timeline) { super(parent, null); this.fxEditor = fxEditor; @@ -331,14 +350,14 @@ class EventsEditor extends Component implements CurveEditorComponent class OverviewEditor extends Component implements CurveEditorComponent { - public var curveEditor : CurveEditor; + public var curveEditor : Timeline; var svg : SVG; var overviewGroup: Element; var overviewKeys: Element; var overviewSelection: Element; - public function new(?parent, curveEditor: CurveEditor) { + public function new(?parent, curveEditor: Timeline) { super(parent, null); this.curveEditor = curveEditor; @@ -373,17 +392,12 @@ class OverviewEditor extends Component implements CurveEditorComponent @:privateAccess this.curveEditor.selectedElements = []; for (c in this.curveEditor.curves){ - c.selected = false; - - if (c.hidden || c.lock || c.blendMode == CurveBlendMode.Blend || c.blendMode == CurveBlendMode.RandomBlend) + if (Timeline.blendMode(c) == CurveBlendMode.Blend) continue; - for (key in c.keys) { - if(key.time >= minT && key.time <= maxT) { - c.selected = true; + for (key in c.data.keys) + if(key.time >= minT && key.time <= maxT) @:privateAccess this.curveEditor.selectedElements.push(key); - } - } } this.curveEditor.refresh(); @@ -452,8 +466,8 @@ class OverviewEditor extends Component implements CurveEditorComponent }); } - function addCurveKeysToOverview(curve: Curve, ?style: Dynamic) { - for(key in curve.keys) { + function addCurveKeysToOverview(curve: EditedCurve, ?style: Dynamic) { + for(key in curve.data.keys) { var kx = xScale*(key.time); var ky = -yScale*(key.value); @@ -465,13 +479,6 @@ class OverviewEditor extends Component implements CurveEditorComponent keyEvent.addClass("selected"); keyEvent.mousedown(function(e) { - if (curve.lock || curve.hidden) return; - - for (c in this.curveEditor.curves) - c.selected = false; - - curve.selected = true; - if(e.which != 1) return; e.preventDefault(); @@ -507,21 +514,10 @@ class OverviewEditor extends Component implements CurveEditorComponent } for (curve in this.curveEditor.curves){ - if (curve.hidden) - continue; - - var style: Dynamic = { 'fill-opacity' : curve.selected ? 1 : 0.5}; - - if (curve.lock || curve.blendMode == CurveBlendMode.Blend) - style = { 'fill-opacity' : curve.selected ? 1 : 0.5}; - - if (curve.hidden || curve.blendMode == CurveBlendMode.RandomBlend) - style = { 'fill-opacity' : 0}; - // We don't want to show keys in overview if the // concerned curve is a blended one - if (curve.blendMode != CurveBlendMode.Blend && curve.blendMode != CurveBlendMode.RandomBlend ) - addCurveKeysToOverview(curve, style); + if (Timeline.blendMode(curve) != CurveBlendMode.Blend) + addCurveKeysToOverview(curve, { 'fill-opacity' : 1 }); } var selectedKeys = @:privateAccess this.curveEditor.selectedElements.filter(item -> item is CurveKey); @@ -589,10 +585,7 @@ class OverviewEditor extends Component implements CurveEditorComponent @:privateAccess this.curveEditor.beforeChange(); for (c in this.curveEditor.curves) - if (c.selected || this.curveEditor.curves.length == 1) { - var previousKeyVal = c.getVal(time); - c.addKey(time, previousKeyVal, c.keyMode); - } + c.data.addKey(time, c.data.getVal(time)); @:privateAccess this.curveEditor.afterChange(); } @@ -605,17 +598,37 @@ class OverviewEditor extends Component implements CurveEditorComponent } class CurvePopup extends hide.comp.Popup { - public var editor : CurveEditor; + public var editor : Timeline; public function new(?parent : Element, undo: hide.ui.UndoHistory) { super(parent); element.addClass("curve-editor-popup"); reflow(); - editor = new CurveEditor(undo, element, false); + editor = new Timeline(undo, element, false); } } -class CurveEditor extends hide.comp.Component { +class Timeline extends hide.comp.Component { + + public static function blendMode(c: EditedCurve) : CurveBlendMode { + return fxCurveOf(c)?.blendMode ?? None; + } + + public static function getCurveColor(name: String, index: Int) : Int { + var suffixes = [[":x", ":h"], [":y", ":s"], [":z", ":l"], [":a"]]; + for (i => s in suffixes) + if (Lambda.exists(s, x -> StringTools.contains(name, x))) + return CURVE_COLORS[i]; + return CURVE_COLORS[index % CURVE_COLORS.length]; + } + + static function fxCurveOf(c: EditedCurve) : hrt.prefab.fx.Curve { + return Std.downcast((cast c : hrt.prefab.Prefab), hrt.prefab.fx.Curve); + } + + static function snapshot(c: EditedCurve) : Dynamic { + return @:privateAccess c.data.copyToDynamic(null); + } public static var CURVE_COLORS: Array = [ 0xff3352, @@ -629,7 +642,7 @@ class CurveEditor extends hide.comp.Component { public var xOffset = 0.; public var yOffset = 0.; - public var curves(default, set) : Array; + public var curves(default, set) : Array; public var undo : hide.ui.UndoHistory; public var lockViewX = false; @@ -690,7 +703,7 @@ class CurveEditor extends hide.comp.Component { tlGroup = svg.group(root, "tlgroup"); markersGroup = svg.group(root, "markers").css({'pointer-events':'none'}); - evaluator = new hrt.prefab.fx.Evaluator([]); + evaluator = new hrt.prefab.fx.Evaluator(); tlGroup.mousedown(function(e) { var lastX = e.clientX; @@ -788,10 +801,10 @@ class CurveEditor extends hide.comp.Component { if(curves == null) return; if(e.keyCode == 46) { beforeChange(); - var newVal = [for(c in curves) [for(k in c.keys) if(selectedElements.indexOf(k) < 0) k]]; + var newVal = [for(c in curves) [for(k in c.data.keys) if(selectedElements.indexOf(k) < 0) k]]; for (i in 0...curves.length) - curves[i].keys = newVal[i]; + curves[i].data.keys = newVal[i]; selectedElements = []; e.preventDefault(); @@ -818,16 +831,10 @@ class CurveEditor extends hide.comp.Component { } - function set_curves(curves: Array) { + function set_curves(curves: Array) { this.curves = curves; - var maxLength = 0.0; - for (c in curves){ - if (c.maxTime > maxLength) - maxLength = c.maxTime; - } - - @:privateAccess lastValue = [for (c in curves) c.serialize()]; + lastValue = [for (c in curves) snapshot(c)]; if (getDisplayState("view") == null) { zoomAll(); } @@ -838,27 +845,10 @@ class CurveEditor extends hide.comp.Component { function addKey(time: Float, ?val: Float) { beforeChange(); - for (c in curves) - if (c.selected || curves.length == 1) { - if(c.minValue < c.maxValue) - val = hxd.Math.clamp(val, c.minValue, c.maxValue); - c.selected = true; - var key = c.addKey(time, val, c.keyMode); - fixKey(key); - } - - afterChange(); - } - - function addPreviewKey(time: Float, ?val: Float) { - beforeChange(); - for (c in curves) - if (c.selected) { - if(c.minValue < c.maxValue) - val = hxd.Math.clamp(val, c.minValue, c.maxValue); - - c.addPreviewKey(time, val); - } + for (c in curves) { + var key = c.data.addKey(time, val); + fixKey(key); + } afterChange(); } @@ -870,26 +860,23 @@ class CurveEditor extends hide.comp.Component { function fixKey(key : CurveKey) { for (c in curves) { - if (!c.selected) - continue; - - var index = c.keys.indexOf(key); + var index = c.data.keys.indexOf(key); // Meaning that this key isn't appartening to this curve if (index == -1) continue; - var prev = c.keys[index-1]; - var next = c.keys[index+1]; + var prev = c.data.keys[index-1]; + var next = c.data.keys[index+1]; inline function addPrevH() { if(key.prevHandle == null) - key.prevHandle = new hrt.prefab.Curve.CurveHandle(prev != null ? (prev.time - key.time) / 3 : -0.5, 0); + key.prevHandle = new hrt.impl.Curve.CurveHandle(prev != null ? (prev.time - key.time) / 3 : -0.5, 0); } inline function addNextH() { if(key.nextHandle == null) - key.nextHandle = new hrt.prefab.Curve.CurveHandle(next != null ? (next.time - key.time) / 3 : -0.5, 0); + key.nextHandle = new hrt.impl.Curve.CurveHandle(next != null ? (next.time - key.time) / 3 : -0.5, 0); } switch(key.mode) { @@ -911,8 +898,6 @@ class CurveEditor extends hide.comp.Component { key.time = 0; if(maxLength > 0 && key.time > maxLength) key.time = maxLength; - if(key.time > c.maxTime) - key.time = c.maxTime; if(prev != null && key.time < prev.time) key.time = prev.time + 0.01; if(next != null && key.time > next.time) @@ -965,17 +950,12 @@ class CurveEditor extends hide.comp.Component { selectedElements = []; for (c in curves){ - if (curves.length > 1) - c.selected = false; - - if (c.hidden || c.lock || c.blendMode == CurveBlendMode.Blend || c.blendMode == CurveBlendMode.RandomBlend) + if (blendMode(c) == CurveBlendMode.Blend) continue; - for (key in c.keys) - if(key.time >= minT && key.time <= maxT && key.value >= minV && key.value <= maxV) { - c.selected = true; + for (key in c.data.keys) + if(key.time >= minT && key.time <= maxT && key.value >= minV && key.value <= maxV) selectedElements.push(key); - } } onSelectionEnd(minT, minV, maxT, maxV); @@ -1056,12 +1036,8 @@ class CurveEditor extends hide.comp.Component { // Compute a surrounding box that encapsulate all the visible curves var bounds = new h2d.col.Bounds(); - for (c in curves) { - if (c.hidden) - continue; - - c.getBounds(bounds); - } + for (c in curves) + c.data.getBounds(bounds); if(bounds.width <= 0) { bounds.xMin = 0.0; @@ -1107,25 +1083,19 @@ class CurveEditor extends hide.comp.Component { } function beforeChange() { - @:privateAccess lastValue = [for (c in curves) c.serialize()]; + lastValue = [for (c in curves) snapshot(c)]; for (c in components) c.beforeChange(); } function afterChange() { - @:privateAccess var newVal = [for (c in curves) c.serialize()]; + var newVal = [for (c in curves) snapshot(c)]; var oldVal = lastValue; undo.change(Custom(function(undo) { - if(undo) { - for (i in 0...curves.length) - curves[i].load(oldVal[i]); - } - else { - for (i in 0...curves.length) - curves[i].load(newVal[i]); - } - @:privateAccess lastValue = [for (c in curves) c.serialize()]; + for (i in 0...curves.length) + @:privateAccess curves[i].data.copyFromDynamic(hrt.prefab.Diff.deepCopy(undo ? oldVal[i] : newVal[i])); + lastValue = [for (c in curves) snapshot(c)]; selectedElements = []; refresh(); onChange(false); @@ -1270,25 +1240,8 @@ class CurveEditor extends hide.comp.Component { var minX = xt(0) - 1; var maxTime = this.duration == 0 ? 5000 : this.duration; - var curvesMaxTime = 0.0; - - // Restrict the "time" to the max of the curves if all curves have a max time - // (usefull when editting Per instance values) - for (curve in curves) { - if (curve.maxTime > 0) { - curvesMaxTime = hxd.Math.max(curvesMaxTime, curve.maxTime); - } else { - curvesMaxTime = 0.0; - break; - } - } - - if (curvesMaxTime > 0) { - maxTime = curvesMaxTime; - } var maxX = xt(maxTime); - svg.line(overlayGroup, xt(1), svg.element.height(), xt(1), 0, { stroke:'#000000', 'stroke-width':'1px', 'stroke-dasharray':'10, 5' }); svg.line(overlayGroup, minX, svg.element.height(), minX, 0, { stroke:'#000000', 'stroke-width':'1px' }); svg.line(overlayGroup, maxX, svg.element.height(), maxX, 0, { stroke:'#000000', 'stroke-width':'1px' }); svg.rect(overlayGroup, 0, 0, Math.max(0,xt(0)), svg.element.height(), { 'fill':'#000000', opacity:0.3}); @@ -1321,7 +1274,7 @@ class CurveEditor extends hide.comp.Component { }); } - function editPopup(curve: Curve, key: CurveKey, top: Float, left: Float) { + function editPopup(curve: EditedCurve, key: CurveKey, top: Float, left: Float) { var popupWidget = new Popup(element, false); var popup = new Element('
@@ -1342,9 +1295,8 @@ class CurveEditor extends hide.comp.Component { popupWidget.position = Inside; popupWidget.onClose = () -> popupWidget.element.remove(); - function setMode(m: hrt.prefab.Curve.CurveKeyMode) { + function setMode(m: hrt.impl.Curve.CurveKeyMode) { key.mode = m; - curve.keyMode = m; fixKey(key); refreshGraph(); } @@ -1390,33 +1342,17 @@ class CurveEditor extends hide.comp.Component { - function drawKeys(curve : Curve, ?style: Dynamic) { - for(key in curve.previewKeys) { - var kx = xScale*(key.time); - var ky = -yScale*(key.value); - var keyHandle = addCircle(keyHandles, kx, ky, style); - keyHandle.addClass("preview"); - } - for(key in curve.keys) { + function drawKeys(curve : EditedCurve, ?style: Dynamic) { + for(key in curve.data.keys) { var kx = xScale*(key.time); var ky = -yScale*(key.value); var keyHandle = addCircle(keyHandles, kx, ky, style); - if(curve.lock) - keyHandle.addClass("no-hover"); - var selected = selectedElements.indexOf(key) >= 0; if(selected) keyHandle.addClass("selected"); if(!anim) { keyHandle.mousedown(function(e) { - if (curve.lock || curve.hidden) return; - - for (c in curves) - c.selected = false; - - curve.selected = true; - if(e.which != 1) return; e.preventDefault(); @@ -1457,12 +1393,6 @@ class CurveEditor extends hide.comp.Component { refreshGraph(); }); keyHandle.contextmenu(function(e) { - if (curve.lock || curve.hidden) return false; - - for (c in curves) - c.selected = false; - - curve.selected = true; var offset = element.offset(); var popup = editPopup(curve, key, e.clientY - offset.top - 50, e.clientX - offset.left); e.preventDefault(); @@ -1484,13 +1414,6 @@ class CurveEditor extends hide.comp.Component { if(anim) return circle; circle.mousedown(function(e) { - if (curve.lock || curve.hidden) return; - - for (c in curves) - c.selected = false; - - curve.selected = true; - if(e.which != 1) return; e.preventDefault(); e.stopPropagation(); @@ -1532,25 +1455,10 @@ class CurveEditor extends hide.comp.Component { redrawCurves(lowQuality); for (curve in curves) { - - var keyStyle: Dynamic = { opacity : curve.selected ? 1 : 0.5}; - - if (curve.lock) { - keyStyle = { opacity : curve.selected ? 1 : 0.5, 'cursor':'default'}; - } - - if (curve.blendMode == CurveBlendMode.Blend) { - keyStyle = { opacity : curve.selected ? 1 : 0.5}; - } - - if (curve.hidden || curve.blendMode == CurveBlendMode.RandomBlend) { - keyStyle = { opacity : 0}; - } - // Blend curve are controlled with parent curve // so we don't want to allow user to use keys on this.s - if (curve.blendMode == CurveBlendMode.None) - drawKeys(curve, keyStyle); + if (blendMode(curve) == CurveBlendMode.None) + drawKeys(curve, { opacity : 1 }); } var selectedKeys = selectedElements.filter(item -> item is CurveKey); @@ -1654,7 +1562,7 @@ class CurveEditor extends hide.comp.Component { var curveGroup = element.find(".curve"); curveGroup.empty(); - function drawCurve(curve : Curve, ?style: Dynamic, bypassRemap: Bool = false, lowQuality = false) { + function drawCurve(curve : EditedCurve, ?style: Dynamic, bypassRemap: Bool = false, lowQuality = false) { // Basic value of xScale is 200 var min = xOffset; var max = min + width / xScale; @@ -1670,37 +1578,27 @@ class CurveEditor extends hide.comp.Component { if (pointBuffer.length != num*2) pointBuffer.resize(num*2); - var v = curve.makeVal(bypassRemap); + var fxCurve = fxCurveOf(curve); + var v = fxCurve == null || (bypassRemap && fxCurve.blendMode == CurveBlendMode.None) ? hrt.prefab.fx.Value.VCurve(curve.data) : fxCurve.makeVal(); evaluator.parameters.clear(); - var paramName = curve.getRemapParameter(); - - var fx = Std.downcast(curve.getRoot(false), hrt.prefab.fx.FX); - - if (curve.blendParam != null) { + if (fxCurve?.param != null) { + var fx = Std.downcast(fxCurve.getRoot(false), hrt.prefab.fx.FX); for (param in fx.parameters) { - if (param.name == curve.blendParam) { - evaluator.parameters.set(curve.blendParam, param.def); - } - } - } - - if (paramName != null) { - for (param in fx.parameters) { - if (param.name == paramName) { - evaluator.parameters.set(paramName, param.def); - var color = '#${StringTools.hex(param.color)}'; - svg.polylineRawArray(curveGroup, [param.def * xScale, (-yOffset) * yScale - height / 2, param.def * xScale, (-yOffset) * yScale + height / 2], {"stroke": color, "stroke-width": "0.5px", "stroke-dasharray":"7, 5" }); + if (param.name == fxCurve.param) { + evaluator.parameters.set(param.name, param.def); + if (fxCurve.blendMode == None) { + var color = '#${StringTools.hex(param.color)}'; + svg.polylineRawArray(curveGroup, [param.def * xScale, (-yOffset) * yScale - height / 2, param.def * xScale, (-yOffset) * yScale + height / 2], {"stroke": color, "stroke-width": "0.5px", "stroke-dasharray":"7, 5" }); + } } } } - if (v == null) throw "wtf"; - inline function getTime(i : Int) { return hxd.Math.lerp(minTime, maxTime, i/(num-1)); } for (i in 0...num) { var t = getTime(i); - var y = evaluator.getFloat(v, getTime(i)); + var y = evaluator.getFloat(v, t); var x = xScale * t; var y = yScale * (-y); @@ -1712,50 +1610,15 @@ class CurveEditor extends hide.comp.Component { svg.polylineRawArray(curveGroup, pointBuffer, style); } - function drawBlendArea(curve : Curve, ?style: Dynamic) { - if (curve.blendMode == CurveBlendMode.RandomBlend) { - var c1: Curve = cast curve.children[0]; - var c2: Curve = cast curve.children[1]; - - var sampleSize = 500 * cast (xScale / 200.0); - var poly = []; - - var ptsC1 = c1.sample(sampleSize); - for(i in 0...ptsC1.length) { - var x = xScale * (c1.duration * i / (ptsC1.length - 1)); - var y = yScale * (-ptsC1[i]); - poly.push(new h2d.col.Point(x, y)); - } - - var ptsC2 = c2.sample(sampleSize); - - var idx = ptsC2.length-1; - while (idx >= 0) { - var x = xScale * (c2.duration * idx / (ptsC2.length - 1)); - var y = yScale * (-ptsC2[idx]); - poly.push(new h2d.col.Point(x, y)); - idx--; - } - - var blendAreaStyle: Dynamic = { opacity : 0.1 , fill : '#FFFFFF', stroke : '#000000'}; - if (c1.lock && c2.lock) blendAreaStyle = { opacity : 0.05 , fill : '#FFFFFF', stroke : '#000000'}; - if (c1.hidden && c2.hidden) blendAreaStyle = { opacity : 0 , fill : '#FFFFFF', stroke : '#000000'}; - - svg.polygon(curveGroup, poly, blendAreaStyle).attr("pointer-events","none"); - } - } - - for (curve in curves){ - if (curve.hidden) - continue; - - var colorInt = curve.color ?? hide.comp.CurveEditor.CURVE_COLORS[0]; - if (curve.blendMode == Blend) { - var root = curve.getRoot(); + for (i => curve in curves){ + var colorInt = getCurveColor(curve.name, i); + var fxCurve = fxCurveOf(curve); + if (blendMode(curve) == Blend) { + var root = fxCurve.getRoot(); var params = Std.downcast(root, hrt.prefab.fx.FX)?.parameters; if (params != null) { for (p in params) { - if (p.name == curve.blendParam) { + if (p.name == fxCurve.param) { colorInt = p.color; break; } @@ -1763,32 +1626,16 @@ class CurveEditor extends hide.comp.Component { } } var color = '#${StringTools.hex(colorInt)}'; - var curveStyle: Dynamic = { opacity : curve.selected ? 1 : 0.5, stroke : color, "stroke-width":'${curve.selected ? 2 : 1}px'}; - var eventStyle: Dynamic = { 'fill-opacity' : curve.selected ? 1 : 0.5}; + var curveStyle: Dynamic = { opacity : 1, stroke : color, "stroke-width":'1px'}; - if (curve.lock) { - curveStyle = { opacity : curve.selected ? 1 : 0.5 , stroke : color, "stroke-width":'${curve.selected ? 2 : 1}px', "stroke-dasharray":"5, 3"}; - eventStyle = { 'fill-opacity' : curve.selected ? 1 : 0.5}; - } - - if (curve.blendMode == CurveBlendMode.Blend) { - curveStyle = { opacity : curve.selected ? 1 : 0.5 , stroke : color, "stroke-width":'${curve.selected ? 2 : 1}px', "stroke-dasharray":"20,10,5,5,5,10"}; - eventStyle = { 'fill-opacity' : curve.selected ? 1 : 0.5}; - } - - if (curve.hidden || curve.blendMode == CurveBlendMode.RandomBlend) { - curveStyle = { opacity : 0}; - eventStyle = { 'fill-opacity' : 0}; - } + if (blendMode(curve) == CurveBlendMode.Blend) + curveStyle = { opacity : 1, stroke : color, "stroke-width":'1px', "stroke-dasharray":"20,10,5,5,5,10"}; drawCurve(curve, curveStyle, true, lowQuality); - if (curve.remapPath != null) { + if (fxCurve?.blendMode == None && fxCurve.param != null) { drawCurve(curve, {opacity : 0.5, "stroke-width":'1px', "stroke-dasharray":"5, 3", stroke : color}, false, lowQuality); } - - // Draw the area where random blend curves will be picked - drawBlendArea(curve); } } diff --git a/hide_js/hide/comp/cdb/Cell.hx b/hide_js/hide/comp/cdb/Cell.hx index 2dce22bda..29e370c30 100644 --- a/hide_js/hide/comp/cdb/Cell.hx +++ b/hide_js/hide/comp/cdb/Cell.hx @@ -1587,14 +1587,13 @@ class Cell { case TCurve: var e = new Element(elementHtml); e.addClass("edit"); - var curveEditor = new hide.comp.CurveEditor.CurvePopup(e, editor.undo); + var curveEditor = new hide.comp.Timeline.CurvePopup(e, editor.undo); var prefabCurve = new hrt.prefab.Curve(null, null); - var linear : Float = cast hrt.prefab.Curve.CurveKeyMode.Linear; + var linear : Float = cast hrt.impl.Curve.CurveKeyMode.Linear; var curve = new cdb.Types.Curve(cast (value ?? [0.0,0.0,cdb.Types.Curve.HandleData,linear,cdb.Types.Curve.HandleData,cdb.Types.Curve.HandleData, 1.0,1.0,cdb.Types.Curve.HandleData,linear,cdb.Types.Curve.HandleData,cdb.Types.Curve.HandleData])); prefabCurve.initFromCDB(curve); - prefabCurve.selected = true; curveEditor.editor.curves = [prefabCurve]; diff --git a/hide_js/hide/comp/curveeditor/CurveDrawing.hx b/hide_js/hide/comp/curveeditor/CurveDrawing.hx new file mode 100644 index 000000000..b1e882a5f --- /dev/null +++ b/hide_js/hide/comp/curveeditor/CurveDrawing.hx @@ -0,0 +1,105 @@ +package hide.comp.curveeditor; + +#if (domkit && js) + +import hide.kit.Curve; + +typedef CurveBounds = { + minX : Float, + maxX : Float, + minY : Float, + maxY : Float +}; + +typedef CurveView = { + > CurveBounds, + w : Float, + h : Float, + pad : Float +}; + +class CurveDrawing { + static inline var SAMPLES = 256; + + public static function x(view: CurveView, t: Float) { + return view.pad + (t - view.minX) / (view.maxX - view.minX) * (view.w - view.pad * 2); + } + + public static function y(view: CurveView, v: Float) { + return view.h - view.pad - (v - view.minY) / (view.maxY - view.minY) * (view.h - view.pad * 2); + } + + /** The value of `t` at `time`, `side` -1 / 1 for the min / max of its random band **/ + public static function trackValue(t: CurveTrack, time: Float, side: Int = 0) { + return t.data.getVal(time) + side * t.spreadAt(time); + } + + public static function frame(tracks: Array, maxTime: Float) : CurveBounds { + var end = maxTime; + if (!Math.isFinite(end)) { + end = 1.; + for (t in tracks) + if (t.data != null && t.data.keys.length > 0) + end = Math.max(end, t.data.keys[t.data.keys.length - 1].time); + } + var bounds = { minX: 0., maxX: end, minY: 0., maxY: 1. }; + for (t in tracks) { + if (t.data == null || t.data.keys.length == 0) + continue; + var r = t.data.getValueRange(); + var lo = r.min, hi = r.max; + if (t.spread != null) { + // the edges of the band are linear in the value of the curve (`CurveTrack.spreadAt`): their extrema are at its lowest / highest + // a spread can be negative (migrated data): the edges are `v -+ |spread|` + var sLow = Math.abs(t.spread.min), sHigh = Math.abs(t.spread.max); + // a flat curve has only the spread of its lowest + if (r.max - r.min <= 1e-9 * Math.max(1, Math.abs(r.max))) + sHigh = sLow; + lo = Math.min(r.min - sLow, r.max - sHigh); + hi = Math.max(r.min + sLow, r.max + sHigh); + } + bounds.minY = Math.min(bounds.minY, lo); + bounds.maxY = Math.max(bounds.maxY, hi); + } + return bounds; + } + + /** The curves of `tracks` and their bands: the `bands` between two tracks, the min and the max of a random curve **/ + public static function drawTracks(svg: SVG, tracks: Array, bands: Array, view: CurveView, maxTime: Float, samples: Int, ?active: CurveTrack) { + var t0 = Math.max(view.minX, 0); + var t1 = Math.min(view.maxX, maxTime); + if (t1 <= t0) + return; + for (b in bands) + if (b.a.data != null && b.b.data != null) + band(svg, view, t0, t1, samples, b.a, 0, b.b, 0); + for (t in tracks) { + if (t.data == null) + continue; + if (t.spread != null) + band(svg, view, t0, t1, samples, t, -1, t, 1); + for (side in (t.spread == null ? [0] : [0, -1, 1])) { + var line = svg.polygon(svg.element, points(view, t0, t1, samples, t, side)); + line.addClass(side == 0 ? "main" : "extreme"); + line.toggleClass("other", active != null && t != active); + } + } + } + + static function band(svg: SVG, view: CurveView, t0: Float, t1: Float, samples: Int, a: CurveTrack, sideA: Int, b: CurveTrack, sideB: Int) { + var back = points(view, t0, t1, samples, b, sideB); + back.reverse(); + svg.polygon2(svg.element, points(view, t0, t1, samples, a, sideA).concat(back)).addClass("band"); + } + + static function points(view: CurveView, t0: Float, t1: Float, samples: Int, track: CurveTrack, side: Int) { + var pts = []; + for (i in 0...samples + 1) { + var t = hxd.Math.lerp(t0, t1, i / samples); + pts.push(new h2d.col.Point(x(view, t), y(view, trackValue(track, t, side)))); + } + return pts; + } +} + +#end diff --git a/hide_js/hide/comp/curveeditor/CurveEditor.hx b/hide_js/hide/comp/curveeditor/CurveEditor.hx new file mode 100644 index 000000000..4ea8f24d3 --- /dev/null +++ b/hide_js/hide/comp/curveeditor/CurveEditor.hx @@ -0,0 +1,637 @@ +package hide.comp.curveeditor; + +#if (domkit && js) + +import hrt.impl.Curve.CurveKeyMode; +import hide.kit.Curve; +import hide.comp.curveeditor.CurveDrawing; + +@:access(hide.kit.Element) +class CurveEditor extends SVG { + static inline var PADDING = 4; + static inline var PIXELS_PER_SAMPLE = 2; + static inline var KEY_SIZE = 6; + static inline var HANDLE_SIZE = 3; + static inline var MAX_LINES = 6; + static inline var BOX_MARGIN = 8; + static inline var BOX_EDGE = 4; + + public var svg(default, null) : js.html.svg.SVGElement; + public var editor(default, null) : Curve; + public var track(default, null) : CurveTrack; + + var editable : Bool; + var minX = 0.; + var maxX = 1.; + var minY = 0.; + var maxY = 1.; + var selection : Array = []; + var box : { x0: Float, y0: Float, x1: Float, y1: Float }; + var lastClick : { key: hrt.impl.Curve.CurveKey, time: Float }; + + public function new(editor: Curve, activeTrack : Int = 0) { + super(); + this.editor = editor; + + svg = cast element[0]; + + setTrack(editor.tracks[activeTrack]); + frame(); + svg.addEventListener("wheel", onWheel); + svg.addEventListener("pointerdown", onPointerDown); + // no autoscroll of the middle button, that pans + svg.addEventListener("mousedown", (e: js.html.MouseEvent) -> if (e.button == 1) e.preventDefault()); + svg.addEventListener("pointermove", (e: js.html.PointerEvent) -> { + if (e.buttons != 0 || !editable) + return; + var m = mapping(); + var p = m.local(e); + var part = keyAt(p.x, p.y, m.w, m.h) == null ? boxPartAt(p.x, p.y, m.w, m.h) : null; + svg.style.cursor = if (part != null) + switch ([part.dx, part.dy]) { + case [0, 0]: "move"; + case [0, _]: "ns-resize"; + case [_, 0]: "ew-resize"; + case [dx, dy]: dx == dy ? "nwse-resize" : "nesw-resize"; + } + else bandEdgeAt(p.x, p.y) != null ? "ns-resize" : ""; + }); + svg.addEventListener("contextmenu", (e: js.html.MouseEvent) -> { + e.preventDefault(); + e.stopPropagation(); + if (editable) + onContextMenu(e); + }); + } + + public function refresh() { + var w = svg.clientWidth, h = svg.clientHeight; + if (w <= 0 || h <= 0) + return; + clear(); + var g = element; + function px(t: Float) return sx(t, w); + function py(v: Float) return sy(v, h); + rectBetween(g, 0, 0, hxd.Math.clamp(px(0), 0, w), h).addClass("outside"); + rectBetween(g, hxd.Math.clamp(px(editor.maxTime), 0, w), 0, w, h).addClass("outside"); + var step = gridStep(maxY - minY); + for (i in Math.ceil(minY / step)...Math.floor(maxY / step) + 1) { + var y = py(i * step); + line(g, 0, y, w, y).addClass(i == 0 ? "zero" : "grid"); + text(g, 2, y - 2 < 9 ? y + 9 : y - 2, gridLabel(i * step)); + } + var step = gridStep(maxX - minX); + for (i in Math.ceil(minX / step)...Math.floor(maxX / step) + 1) { + var x = px(i * step); + line(g, x, 0, x, h).addClass(i == 0 ? "zero" : "grid"); + text(g, x + 2, h - 3, gridLabel(i * step)); + } + + var view : CurveView = { minX: minX, maxX: maxX, minY: minY, maxY: maxY, w: w, h: h, pad: PADDING }; + CurveDrawing.drawTracks(this, editor.tracks, editor.bands, view, editor.maxTime, Math.ceil(w / PIXELS_PER_SAMPLE), track); + + function key(t: CurveTrack, k: hrt.impl.Curve.CurveKey) return square(g, px(k.time), py(k.value), KEY_SIZE).addClass("key"); + for (t in editor.tracks) + if (t != track && t.data != null) + for (k in t.data.keys) + key(t, k).addClass("other"); + if (!editable) + return; + selection = selection.filter(k -> track.data.keys.contains(k)); + var b = box == null ? selectionBox(w, h) : null; + if (b != null) + rectBetween(g, b.x0, b.y0, b.x1, b.y1).addClass("selection-box"); + if (selection.length == 1) { + var k = selection[0]; + for (hd in handles(k)) { + var x = px(k.time + hd.h.dt), y = py(k.value + hd.h.dv); + line(g, px(k.time), py(k.value), x, y).addClass("tangent"); + circle(g, x, y, HANDLE_SIZE).addClass("handle"); + } + } + for (k in track.data.keys) + key(track, k).toggleClass("selected", selection.contains(k)); + if (box != null) + rectBetween(g, box.x0, box.y0, box.x1, box.y1).addClass("keys-box"); + } + + + public function onKeyDown(e: js.html.KeyboardEvent) { + if (e.ctrlKey) + return; + switch (e.key) { + case "f": + frame(); + refresh(); + case "Delete" if (editable && selection.length > 0): + track.deleteKeys(selection); + default: + return; + } + e.preventDefault(); + e.stopPropagation(); + } + + public function onPointerDown(e: js.html.PointerEvent) { + e.preventDefault(); + e.stopPropagation(); + (cast svg.closest("kit-curve-view") : js.html.Element).focus(); + if (e.button == 1) { + pan(e); + return; + } + if (editor.isDisabled() || e.button != 0) + return; + var m = mapping(); + var p = m.local(e); + var other = (editable ? keyAt(p.x, p.y, m.w, m.h) : null) == null ? otherTrackAt(p.x, p.y, m.w, m.h) : null; + if (other != null) { + setTrack(other); + refresh(); + onPointerDown(e); + return; + } + if (!editable) + return; + var curve = track.data; + + var hd = handleAt(p.x, p.y, m.w, m.h); + if (hd != null) { + dragHandle(e, selection[0], hd.h, hd.next); + return; + } + var key = keyAt(p.x, p.y, m.w, m.h); + // the svg is redrawn by the first click: the double click is found here, not by a dblclick event + var now = haxe.Timer.stamp(); + var isDoubleClick = !e.shiftKey && lastClick != null && lastClick.key == key && now - lastClick.time < 0.4; + lastClick = isDoubleClick ? null : { key: key, time: now }; + if (key == null) { + // Ctrl + click or double click in the box adds a key, on its edges scales the keys around their center + var part = e.shiftKey ? null : boxPartAt(p.x, p.y, m.w, m.h); + if (part != null && !((e.ctrlKey || isDoubleClick) && part.dx == 0 && part.dy == 0)) { + if (part.dx == 0 && part.dy == 0) { + var first = selection[0]; + for (k in selection) + if (k.time < first.time) + first = k; + moveKeys(e, first); + } else + scaleKeys(e, part); + return; + } + var edge = e.ctrlKey || e.shiftKey ? null : bandEdgeAt(p.x, p.y); + if (edge != null) { + scaleBand(e, edge); + return; + } + if (!e.ctrlKey && !isDoubleClick) { + selectBox(e, p, e.shiftKey); + return; + } + var t = hxd.Math.clamp(m.time(p.x), 0, editor.maxTime); + track.edit(() -> { + key = curve.addKey(t, m.value(p.y)); + track.fixKey(key); + }, true); + selection = [key]; + } else { + if (e.shiftKey) { + if (!selection.remove(key)) + selection.push(key); + refresh(); + return; + } + if (isDoubleClick) { + // opened on release: the popover would be light dismissed by a release outside of it + drag(e, (_) -> {}, () -> editKey(key, e.clientX, e.clientY)); + return; + } + if (!selection.contains(key)) + selection = [key]; + refresh(); + } + moveKeys(e, key); + } + + public function onContextMenu(e: js.html.MouseEvent) { + if (editor.isDisabled()) + return; + var curve = track.data; + var m = mapping(); + var p = m.local(e); + var key = keyAt(p.x, p.y, m.w, m.h); + if (key != null) { + if (!selection.contains(key)) + selection = [key]; + refresh(); + keyMenu(e, key); + return; + } + menu(e, [{ label: "Add key", keys: "Ctrl + click", click: () -> { + var t = hxd.Math.clamp(m.time(p.x), 0, editor.maxTime); + track.edit(() -> { + var k = curve.addKey(t, m.value(p.y)); + track.fixKey(k); + selection = [k]; + }, false); + } }]); + } + + public function onWheel(e: js.html.WheelEvent) { + e.preventDefault(); + e.stopPropagation(); + var factor = e.deltaY + e.deltaX < 0 ? 1 / 1.125 : 1.125; + var m = mapping(); + var p = m.local(e); + var t = m.time(p.x), v = m.value(p.y); + if (!e.altKey) { + minX = t + (minX - t) * factor; + maxX = t + (maxX - t) * factor; + } + if (!e.ctrlKey) { + minY = v + (minY - v) * factor; + maxY = v + (maxY - v) * factor; + } + refresh(); + } + + public dynamic function onTrackChange() {} + + + public function setTrack(t: CurveTrack) { + if (t == track) + return; + track = t; + editable = track.data != null; + selection = []; + onTrackChange(); + } + + static inline function near(x: Float, y: Float, px: Float, py: Float, d: Float) return Math.abs(px - x) <= d && Math.abs(py - y) <= d; + + static function handles(k: hrt.impl.Curve.CurveKey) : Array<{ h: hrt.impl.Curve.CurveHandle, next: Bool }> { + if (k.mode == CurveKeyMode.Linear || k.mode == CurveKeyMode.Constant) + return []; + return [for (next in [false, true]) { + var h = next ? k.nextHandle : k.prevHandle; + if (h != null) { h: h, next: next }; + }]; + } + + public function keepView(other: CurveEditor) { + minX = other.minX; + maxX = other.maxX; + minY = other.minY; + maxY = other.maxY; + } + + public function frame() { + var bounds = CurveDrawing.frame(editor.tracks, editor.maxTime); + minX = bounds.minX; + maxX = bounds.maxX; + minY = bounds.minY; + maxY = bounds.maxY; + } + + + + function pan(e: js.html.PointerEvent) { + var m = mapping(); + var start = m.local(e), startX = [minX, maxX], startY = [minY, maxY]; + drag(e, (e) -> { + var p = m.local(e); + var dt = (p.x - start.x) / (m.w - PADDING * 2) * (startX[1] - startX[0]); + var dv = (p.y - start.y) / (m.h - PADDING * 2) * (startY[1] - startY[0]); + minX = startX[0] - dt; + maxX = startX[1] - dt; + minY = startY[0] + dv; + maxY = startY[1] + dv; + refresh(); + }, () -> {}); + } + + inline function sx(t: Float, w: Float) return PADDING + (t - minX) / (maxX - minX) * (w - PADDING * 2); + inline function sy(v: Float, h: Float) return h - PADDING - (v - minY) / (maxY - minY) * (h - PADDING * 2); + + static function gridStep(size: Float) { + var i = Math.floor(Math.log(size / MAX_LINES) / Math.log(10)) * 3; + var step = 0.; + do { + step = [1, 2, 5][((i % 3) + 3) % 3] * Math.pow(10, Math.floor(i++ / 3)); + } while (size / step > MAX_LINES); + return step; + } + + /** `v` without the float noise of the grid steps (0.30000000000000004) **/ + static function gridLabel(v: Float) return Std.string(Math.round(v * 1e6) / 1e6); + + + + function mapping() { + var rect = svg.getBoundingClientRect(); + var w = rect.width, h = rect.height; + return { + rect: rect, + w: w, + h: h, + local: (e: js.html.MouseEvent) -> { x: e.clientX - rect.left, y: e.clientY - rect.top }, + time: (x: Float) -> minX + (x - PADDING) / (w - PADDING * 2) * (maxX - minX), + value: (y: Float) -> minY + (h - PADDING - y) / (h - PADDING * 2) * (maxY - minY), + }; + } + + function keyAt(x: Float, y: Float, w: Float, h: Float, ?t: CurveTrack) { + t ??= track; + return Lambda.find(t.data.keys, k -> near(x, y, sx(k.time, w), sy(k.value, h), KEY_SIZE)); + } + + function otherTrackAt(x: Float, y: Float, w: Float, h: Float) { + return Lambda.find(editor.tracks, t -> t != track && t.data != null && keyAt(x, y, w, h, t) != null); + } + + function handleAt(x: Float, y: Float, w: Float, h: Float) { + if (selection.length != 1) + return null; + var k = selection[0]; + return Lambda.find(handles(k), hd -> near(x, y, sx(k.time + hd.h.dt, w), sy(k.value + hd.h.dv, h), HANDLE_SIZE + 2)); + } + + function selectionBox(w: Float, h: Float) { + if (selection.length < 2) + return null; + var x0 = Math.POSITIVE_INFINITY, y0 = Math.POSITIVE_INFINITY, x1 = Math.NEGATIVE_INFINITY, y1 = Math.NEGATIVE_INFINITY; + for (k in selection) { + var x = sx(k.time, w), y = sy(k.value, h); + x0 = Math.min(x0, x); + y0 = Math.min(y0, y); + x1 = Math.max(x1, x); + y1 = Math.max(y1, y); + } + return { x0: x0 - BOX_MARGIN, y0: y0 - BOX_MARGIN, x1: x1 + BOX_MARGIN, y1: y1 + BOX_MARGIN }; + } + + /** + The part of the box of the selected keys at (x, y): its inside (0, 0), an edge (`dx`: -1 left, 1 right, `dy`: -1 top, + 1 bottom, 0 for the other axis) or a corner. Null out of it + **/ + function boxPartAt(x: Float, y: Float, w: Float, h: Float) : { dx: Int, dy: Int } { + var b = selectionBox(w, h); + if (b == null || x < b.x0 - BOX_EDGE || x > b.x1 + BOX_EDGE || y < b.y0 - BOX_EDGE || y > b.y1 + BOX_EDGE) + return null; + inline function side(v: Float, v0: Float, v1: Float) return Math.abs(v - v0) <= BOX_EDGE ? -1 : Math.abs(v - v1) <= BOX_EDGE ? 1 : 0; + return { dx: side(x, b.x0, b.x1), dy: side(y, b.y0, b.y1) }; + } + + function selectBox(e: js.html.PointerEvent, start: { x: Float, y: Float }, add: Bool) { + var m = mapping(); + var initial = add ? selection.copy() : []; + box = { x0: start.x, y0: start.y, x1: start.x, y1: start.y }; + selection = initial.copy(); + refresh(); + drag(e, (e) -> { + var p = m.local(e); + box.x1 = p.x; + box.y1 = p.y; + var x0 = Math.min(box.x0, box.x1), x1 = Math.max(box.x0, box.x1), y0 = Math.min(box.y0, box.y1), y1 = Math.max(box.y0, box.y1); + selection = initial.copy(); + for (k in track.data.keys) { + var x = sx(k.time, m.w), y = sy(k.value, m.h); + if (x >= x0 && x <= x1 && y >= y0 && y <= y1 && !selection.contains(k)) + selection.push(k); + } + refresh(); + }, () -> { + box = null; + refresh(); + }); + } + + function bandEdgeAt(x: Float, y: Float) : Null { + if (track.spread == null) + return null; + var m = mapping(); + var t = m.time(x); + if (t < 0 || t > editor.maxTime) + return null; + for (side in [1, -1]) + if (Math.abs(sy(CurveDrawing.trackValue(track, t, side), m.h) - y) <= KEY_SIZE) + return side; + return null; + } + + /** + Drags the `edge` of the band of a random curve (1: its max, -1: its min) up and down where it is grabbed: the band gets + that half width there, the curve stays (`CurveTrack.moveEdge`). An undo step + **/ + function scaleBand(e: js.html.PointerEvent, edge: Int) { + var m = mapping(); + var t = hxd.Math.clamp(m.time(m.local(e).x), 0, 1); + drag(e, (e) -> { + var v = m.value(m.local(e).y); + track.edit(() -> track.moveEdge(t, edge, e.ctrlKey ? Math.round(v * 10) / 10 : v), true); + }, () -> track.edit(null, false)); + } + + function moveKeys(e: js.html.PointerEvent, grabbed: hrt.impl.Curve.CurveKey) { + var m = mapping(); + var curve = track.data; + var keys = selection.copy(); + var start = [for (k in keys) { time: k.time, value: k.value }]; + var grabbedStart = start[keys.indexOf(grabbed)]; + var minDt = Math.NEGATIVE_INFINITY, maxDt = Math.POSITIVE_INFINITY; + for (j => k in keys) { + var i = curve.keys.indexOf(k); + var prev = curve.keys[i - 1], next = curve.keys[i + 1]; + if (prev == null || !keys.contains(prev)) + minDt = Math.max(minDt, (prev?.time ?? 0) - start[j].time); + if (next == null || !keys.contains(next)) + maxDt = Math.min(maxDt, (next?.time ?? editor.maxTime) - start[j].time); + } + var p0 = m.local(e); + var t0 = m.time(p0.x), v0 = m.value(p0.y); + drag(e, (e) -> { + var p = m.local(e); + var t = grabbedStart.time + m.time(p.x) - t0, v = grabbedStart.value + m.value(p.y) - v0; + if (e.ctrlKey) { + t = Math.round(t * 10) / 10; + v = Math.round(v * 10) / 10; + } + var dt = e.shiftKey ? 0 : hxd.Math.clamp(t - grabbedStart.time, minDt, maxDt); + var dv = e.altKey ? 0 : v - grabbedStart.value; + track.edit(() -> for (j => k in keys) { + k.time = start[j].time + dt; + k.value = start[j].value + dv; + }, true); + }, () -> track.edit(null, false)); + } + + function scaleKeys(e: js.html.PointerEvent, part: { dx: Int, dy: Int }) { + var m = mapping(); + var curve = track.data; + var keys = selection.copy(); + var start = [for (k in keys) { + time: k.time, + value: k.value, + handles: [for (h in [k.prevHandle, k.nextHandle]) if (h != null) { h: h, dt: h.dt, dv: h.dv }], + }]; + var tMin = Math.POSITIVE_INFINITY, tMax = Math.NEGATIVE_INFINITY, vMin = Math.POSITIVE_INFINITY, vMax = Math.NEGATIVE_INFINITY; + for (s in start) { + tMin = Math.min(tMin, s.time); + tMax = Math.max(tMax, s.time); + vMin = Math.min(vMin, s.value); + vMax = Math.max(vMax, s.value); + } + var scaleTime = part.dx != 0 && tMax - tMin > 1e-9; + var edgeT = part.dx < 0 ? tMin : tMax, anchorT = part.dx < 0 ? tMax : tMin; + var scaleValue = part.dy != 0 && vMax - vMin > 1e-9; + var maxOnTop = sy(vMax, m.h) < sy(vMin, m.h); + var edgeV = (part.dy < 0) == maxOnTop ? vMax : vMin, anchorV = edgeV == vMax ? vMin : vMax; + if (!scaleTime && !scaleValue) + return; + + function timeLimits(anchor: Float) { + var sMin = 0., sMax = Math.POSITIVE_INFINITY; + for (j => k in keys) { + var i = curve.keys.indexOf(k); + var prev = curve.keys[i - 1], next = curve.keys[i + 1]; + var lo = prev == null ? 0 : keys.contains(prev) ? Math.NEGATIVE_INFINITY : prev.time; + var hi = next == null ? editor.maxTime : keys.contains(next) ? Math.POSITIVE_INFINITY : next.time; + var d = start[j].time - anchor; + if (d > 1e-9) { + sMin = Math.max(sMin, (lo - anchor) / d); + sMax = Math.min(sMax, (hi - anchor) / d); + } else if (d < -1e-9) { + sMin = Math.max(sMin, (hi - anchor) / d); + sMax = Math.min(sMax, (lo - anchor) / d); + } + } + return { min: sMin, max: sMax }; + } + var centerT = (tMin + tMax) * 0.5, centerV = (vMin + vMax) * 0.5; + var limits = timeLimits(anchorT), centerLimits = timeLimits(centerT); + + var p0 = m.local(e); + var t0 = m.time(p0.x), v0 = m.value(p0.y); + drag(e, (e) -> { + var p = m.local(e); + var at = e.ctrlKey ? centerT : anchorT, av = e.ctrlKey ? centerV : anchorV; + var l = e.ctrlKey ? centerLimits : limits; + var st = scaleTime ? hxd.Math.clamp((edgeT + m.time(p.x) - t0 - at) / (edgeT - at), l.min, l.max) : 1.; + var sv = scaleValue ? Math.max((edgeV + m.value(p.y) - v0 - av) / (edgeV - av), 0) : 1.; + track.edit(() -> for (j => k in keys) { + var s = start[j]; + if (scaleTime) + k.time = at + (s.time - at) * st; + if (scaleValue) + k.value = av + (s.value - av) * sv; + for (hs in s.handles) { + hs.h.dt = hs.dt * st; + hs.h.dv = hs.dv * sv; + } + }, true); + }, () -> track.edit(null, false)); + } + + + + + /** Drags `handle` of `k`, the other one following it in the Aligned mode, opposite and of the same length on screen **/ + function dragHandle(e: js.html.PointerEvent, k: hrt.impl.Curve.CurveKey, handle: hrt.impl.Curve.CurveHandle, next: Bool) { + var m = mapping(); + var other = next ? k.prevHandle : k.nextHandle; + inline function kx() return sx(k.time, m.w); + inline function ky() return sy(k.value, m.h); + var otherLength = other == null ? 0. : hxd.Math.distance(sx(k.time + other.dt, m.w) - kx(), sy(k.value + other.dv, m.h) - ky()); + drag(e, (e) -> { + var p = m.local(e); + var x = next ? Math.max(p.x, kx()) : Math.min(p.x, kx()); + track.edit(() -> { + handle.dt = m.time(x) - k.time; + handle.dv = m.value(p.y) - k.value; + if (k.mode == CurveKeyMode.Aligned && other != null) { + var dx = x - kx(), dy = p.y - ky(); + var len = hxd.Math.distance(dx, dy); + if (len > 1e-6) { + var ox = kx() - dx / len * otherLength, oy = ky() - dy / len * otherLength; + other.dt = m.time(ox) - k.time; + other.dv = m.value(oy) - k.value; + } + } + }, true); + }, () -> track.edit(null, false)); + } + + function drag(e: js.html.PointerEvent, move: js.html.PointerEvent -> Void, end: () -> Void) { + svg.setPointerCapture(e.pointerId); + svg.onpointermove = move; + svg.onpointerup = (e: js.html.PointerEvent) -> { + svg.onpointermove = null; + svg.onpointerup = null; + svg.releasePointerCapture(e.pointerId); + end(); + } + } + + function keyMenu(e: js.html.MouseEvent, k: hrt.impl.Curve.CurveKey) { + var curve = track.data; + var keys = selection.copy(); + var items : Array = [ + { label: keys.length == 1 ? "Delete key" : "Delete keys", keys: "Delete", click: () -> track.deleteKeys(keys), enabled: curve.keys.length > keys.length }, + { label: "Edit key", click: () -> editKey(k, e.clientX, e.clientY), enabled: keys.length == 1 }, + { isSeparator: true }, + ]; + for (mode in [CurveKeyMode.Aligned, CurveKeyMode.Free, CurveKeyMode.Linear, CurveKeyMode.Constant]) { + items.push({ + label: Std.string(["Aligned", "Free", "Linear", "Constant"][(cast mode : Int)]), + radio: () -> Lambda.foreach(keys, k -> k.mode == mode), + click: () -> track.edit(() -> { + for (k in keys) { + k.mode = mode; + track.fixKey(k); + } + }, false), + }); + } + menu(e, items); + } + + /** + A context menu at the mouse in the document of the svg (the curve window), from the svg: the target of the event + (a key, the curve) is replaced when the svg is redrawn, and a menu closes once its element leaves the page + **/ + function menu(e: js.html.MouseEvent, items: Array) { + new hide.comp.ContextMenu(items, cast svg, null, { x: e.clientX, y: e.clientY }); + } + + function editKey(k: hrt.impl.Curve.CurveKey, x: Float, y: Float) { + var curve = track.data; + var parent = svg.parentElement; + var popup = new hide.comp.Popup(new hide.Element(parent), false); + var content = new hide.Element('
+
+
+
').appendTo(popup.element); + var rect = parent.getBoundingClientRect(); + popup.position = Inside; + popup.offsetX = x + 8 - rect.left; + popup.offsetY = y - 20 - rect.top; + popup.onClose = () -> popup.element.remove(); + var t = content.find(".t"), v = content.find(".v"); + t.val(hxd.Math.fmt(k.time)); + v.val(hxd.Math.fmt(k.value)); + function set(f: Float -> Void, input: hide.Element) { + var f2 = Std.parseFloat(input.val()); + if (Math.isNaN(f2)) + return; + track.edit(() -> f(f2), false); + } + t.change((_) -> set(f -> { + var i = curve.keys.indexOf(k); + k.time = hxd.Math.clamp(f, curve.keys[i - 1]?.time ?? 0, curve.keys[i + 1]?.time ?? editor.maxTime); + }, t)); + v.change((_) -> set(f -> k.value = f, v)); + t.focus(); + } +} + +#end diff --git a/hide_js/hide/comp/curveeditor/CurveThumbnail.hx b/hide_js/hide/comp/curveeditor/CurveThumbnail.hx new file mode 100644 index 000000000..20b651070 --- /dev/null +++ b/hide_js/hide/comp/curveeditor/CurveThumbnail.hx @@ -0,0 +1,39 @@ +package hide.comp.curveeditor; + +#if (domkit && js) + +import hide.kit.Curve; + +class CurveThumbnail extends hide.comp.Component { + static inline var SAMPLES = 64; + + var drawing : SVG; + var tracks : Array; + var bands : Array; + var maxTime : Float; + + public function new(parent: js.html.Element, tracks: Array, bands: Array, maxTime: Float) { + var drawing = new SVG(); + super(null, drawing.element); + this.drawing = drawing; + this.tracks = tracks; + this.bands = bands; + this.maxTime = maxTime; + var svg = drawing.element[0]; + svg.setAttribute("viewBox", "0 0 1 1"); + svg.setAttribute("preserveAspectRatio", "none"); + parent.appendChild(svg); + refresh(); + } + + public function refresh() { + drawing.clear(); + var bounds = CurveDrawing.frame(tracks, maxTime); + var view : CurveDrawing.CurveView = { minX: bounds.minX, maxX: bounds.maxX, minY: bounds.minY, maxY: bounds.maxY, w: 1, h: 1, pad: 0 }; + var zero = CurveDrawing.y(view, 0); + drawing.line(drawing.element, 0, zero, 1, zero).addClass("zero"); + CurveDrawing.drawTracks(drawing, tracks, bands, view, maxTime, SAMPLES); + } +} + +#end diff --git a/hide_js/hide/html/comp/style/TabContainer.css b/hide_js/hide/html/comp/style/TabContainer.css index 7e596ac68..fb880dadc 100644 --- a/hide_js/hide/html/comp/style/TabContainer.css +++ b/hide_js/hide/html/comp/style/TabContainer.css @@ -37,7 +37,7 @@ .header { display: flex; flex-shrink: 0; - height: 28px; + height: 24px; background: var(--theme-crust); } diff --git a/hide_js/hide/view/FXEditor.hx b/hide_js/hide/view/FXEditor.hx index 099de4c12..a974e63ae 100644 --- a/hide_js/hide/view/FXEditor.hx +++ b/hide_js/hide/view/FXEditor.hx @@ -4,7 +4,7 @@ using Lambda; import hide.Element; import hrt.prefab.Prefab in PrefabElement; -import hrt.prefab.Curve; +import hrt.prefab.fx.Curve; import hrt.prefab.fx.Event; import hide.tools.CameraController.CamController; @@ -155,7 +155,7 @@ private class FXSceneEditor extends hide.comp.SceneEditor { if (curve == null || (curve.blendMode != Blend && curve.blendMode != Reference)) return buttons; - var paramName = curve.blendParam; + var paramName = curve.param; var color = 0xFFFFFF; var icon = "ico-random"; var missing = false; @@ -169,7 +169,7 @@ private class FXSceneEditor extends hide.comp.SceneEditor { color = param?.color; } else { - var ref = (cast this.parent.data: hrt.prefab.Prefab).locatePrefab(curve.blendParam); + var ref = (cast this.parent.data: hrt.prefab.Prefab).locatePrefab(curve.refPath); if (ref == null) missing = true; icon = "ico-link"; @@ -348,7 +348,7 @@ class FXEditor extends hide.view.FileView { var selectMax : Float; var previewMin : Float; var previewMax : Float; - var curveEditor : hide.comp.CurveEditor; + var curveEditor : hide.comp.Timeline; var afterPanRefreshes : ArrayVoid> = []; var statusText : h2d.Text; var forceTick : Bool = false; @@ -720,53 +720,14 @@ class FXEditor extends hide.view.FileView { if (pname == "speed" && p is hrt.prefab.fx.SubFX) rebuildAnimPanel(); - if (pname == "blendMode" || pname == "remapPath") { - var curve = Std.downcast(p, Curve); - - function removeCurves() { - var toRemove: Array = []; - for (child in curve.children) { - var c = Std.downcast(child, Curve); - if (c != null) - toRemove.push(c); - } - - while (toRemove.length > 0) { - var c = toRemove.pop(); - //c.removeInstance(); - c.remove(); - } - } - - if (curve != null) { - if (curve.blendMode == CurveBlendMode.Blend || curve.blendMode == CurveBlendMode.RandomBlend) { - if (curve.children.length != 2) { - removeCurves(); - - // We're currently supporting blending with only 2 curves - for (i in 0...2) { - var c = new Curve(curve, null); - c.name = '$i'; - if (i == 0) { - for (k in curve.keys) { - var newK = new hrt.prefab.Curve.CurveKey(); - @:privateAccess newK.copyFromOther(k); - c.keys.push(newK); - } - } - } - } - } - else { - removeCurves(); - } - } + if (pname == "blendMode" || pname == "param") { + Std.downcast(p, Curve)?.syncDatas(); sceneEditor.queueRebuild(@:privateAccess sceneEditor.sceneData); rebuildAnimPanel(); } - if (pname == "blendParam") { + if (pname == "refPath") { sceneEditor.queueRebuild(@:privateAccess sceneEditor.sceneData); rebuildAnimPanel(); } @@ -827,15 +788,15 @@ class FXEditor extends hide.view.FileView { if (this.curveEditor != null) previousTime = @:privateAccess this.curveEditor.currentTime; - this.curveEditor = new hide.comp.CurveEditor(this.undo, rightPanel); + this.curveEditor = new hide.comp.Timeline(this.undo, rightPanel); this.curveEditor.onRefreshProps = () -> { skipRebuildPannel++; @:privateAccess sceneEditor.selectElements(sceneEditor.selectedPrefabs, NoHistory); skipRebuildPannel--; } - var overviewEditor = new hide.comp.CurveEditor.OverviewEditor(rightPanel, this.curveEditor); - var eventEditor = new hide.comp.CurveEditor.EventsEditor(rightPanel, this, this.curveEditor); + var overviewEditor = new hide.comp.Timeline.OverviewEditor(rightPanel, this.curveEditor); + var eventEditor = new hide.comp.Timeline.EventsEditor(rightPanel, this, this.curveEditor); for (e in events) eventEditor.events.push(e); @@ -844,7 +805,6 @@ class FXEditor extends hide.view.FileView { for (curve in curves){ var dispKey = getPath() + "/" + curve.getAbsPath(true); - curve.maxTime = data.duration == 0 ? 5000 : data.duration; this.curveEditor.saveDisplayKey = dispKey; this.curveEditor.requestXZoom = function(xMin, xMax) { @@ -858,48 +818,15 @@ class FXEditor extends hide.view.FileView { this.curveEditor.xScale = xScale; } - if(["visibility", "s", "l", "a"].indexOf(curve.name.split(":").pop()) >= 0) { - curve.minValue = 0; - curve.maxValue = 1; - } - if(curve.name.indexOf("Rotation") >= 0 || curve.name.indexOf("Local Rotation") >= 0) { - curve.minValue = -360; - curve.maxValue = 360; - } - var shader = curve.parent.to(hrt.prefab.Shader); - if(shader != null) { - var sh = shader.getShaderDefinition(); - if(sh != null) { - var v = sh.data.vars.find(v -> v.kind == Param && v.name == curve.name); - if(v != null && v.qualifiers != null) { - for( q in v.qualifiers ) - switch( q ) { - case Range(rmin, rmax): - curve.minValue = rmin; - curve.maxValue = rmax; - default: - } - } - } - } this.curveEditor.xOffset = xOffset; this.curveEditor.xScale = xScale; - var isInstance = isInstanceCurve(curve); - if (isInstance) { - var name = curve.name.split(":")[0]; - var param = hrt.prefab.fx.Emitter.PARAMS[name]; - if (param != null) { - isInstance = param.instance; - } else { - isInstance = false; - } - } - - if(isInstance) { - curve.maxTime = curve.name.indexOf("OverTime") >= 0 ? 5000 : 1.0; - } - this.curveEditor.curves.push(curve); + // a curve of two datas: each one edited apart, the blend drawn as it is + if (curve.blendMode != CurveBlendMode.Range) + this.curveEditor.curves.push(curve); + if (curve.datas.length > 1) + for (i in 0...curve.datas.length) + this.curveEditor.curves.push(new hide.comp.Timeline.CurveData(curve, i)); this.curveEditor.onChange = function(anim) { //refreshDopesheet(); forceTick = true; @@ -957,22 +884,6 @@ class FXEditor extends hide.view.FileView { return null; } - function areAllChildrenLocked(secRoot : PrefabElement) { - for (c in secRoot.flatten(Curve)) - if(!c.lock) - return false; - - return true; - } - - function areAllChildrenHidden(secRoot : PrefabElement) { - for (c in secRoot.flatten(Curve)) - if(!c.hidden) - return false; - - return true; - } - function addVisibilityButtonListener(parentEl: Element, affectedElements : Array) { var visibilityEl = parentEl.find(".visibility"); var saveKey = affectedElements.length == 1 ? affectedElements[0].getAbsPath(true) : parentEl.hasClass("section") ? section.root.getAbsPath(true) : affectedElements[0]?._parent.getAbsPath(true) ?? "null"; @@ -1042,10 +953,6 @@ class FXEditor extends hide.view.FileView { function addOnSelectListener(parentEl: Element, affectedElements : Array) { var trackEl = parentEl.find(".track-header"); trackEl.click(function(e) { - - for (c in this.curveEditor.curves) - c.selected = false; - for (c in affectedElements) c.selected = true; @@ -1103,12 +1010,9 @@ class FXEditor extends hide.view.FileView { var allElements: Array = []; var curves = section.root.flatten(Curve); - for (c in curves) - allElements.push(c); if (section.root is Curve) { curves.push(cast section.root); - allElements.push(cast section.root); } var events: Array = []; @@ -1122,14 +1026,17 @@ class FXEditor extends hide.view.FileView { allElements.push(cast section.root); } + var areEventsLocked = events.length > 0 && Lambda.foreach(events, e -> e.lock); + var areEventsHidden = events.length > 0 && Lambda.foreach(events, e -> e.hidden); + var sectionEl = new Element('
-
+
${section.root is Curve ?'': '
'} -
+
'); @@ -1144,32 +1051,17 @@ class FXEditor extends hide.view.FileView { for (i in 0...section.curves.length) { var c = section.curves[i]; - var curveColor = hide.comp.CurveEditor.CURVE_COLORS[i]; - if (StringTools.contains(c.name, ":x") || StringTools.contains(c.name, ":h")) curveColor = hide.comp.CurveEditor.CURVE_COLORS[0]; - if (StringTools.contains(c.name, ":y") || StringTools.contains(c.name, ":s")) curveColor = hide.comp.CurveEditor.CURVE_COLORS[1]; - if (StringTools.contains(c.name, ":z") || StringTools.contains(c.name, ":l")) curveColor = hide.comp.CurveEditor.CURVE_COLORS[2]; - if (StringTools.contains(c.name, ":w") || StringTools.contains(c.name, ":a")) curveColor = hide.comp.CurveEditor.CURVE_COLORS[3]; - - // Assign same color to curve and curve's header - c.color = curveColor; - var hexColor = '#${StringTools.hex(curveColor)}'; - - var colorStyle = c.selected ? "style = color:#d59320" : ""; + var hexColor = '#${StringTools.hex(hide.comp.Timeline.getCurveColor(c.name, i))}'; var trackEl = new Element('
-
- -
+
'); sectionEl.append(trackEl); - addVisibilityButtonListener(trackEl, [ c ]); - addLockButtonListener(trackEl, [ c ]); - addOnSelectListener(trackEl, [ c ]); addFoldButtonListener(trackEl); } @@ -1203,27 +1095,18 @@ class FXEditor extends hide.view.FileView { if (section.root is Curve) { var c: Curve = cast section.root; - - // We don't want to show an header for the blending curve, - // just for the parent blend curves. - if (c.blendMode != CurveBlendMode.Blend && c.blendMode != CurveBlendMode.RandomBlend) { - var hexColor = '#${StringTools.hex(c.color)}'; - var colorStyle = c.selected ? "style = color:#d59320" : ""; + { + var hexColor = '#${StringTools.hex(hide.comp.Timeline.getCurveColor(c.name, 0))}'; var trackEl = new Element('
-
- -
+
'); sectionEl.append(trackEl); - addVisibilityButtonListener(trackEl, curves); - addLockButtonListener(trackEl, curves); - addOnSelectListener(trackEl, curves); addFoldButtonListener(trackEl); } } @@ -1302,29 +1185,29 @@ class FXEditor extends hide.view.FileView { var curvesToDraw : Array = []; var eventsToDraw : Array = []; - function getSection(?root : PrefabElement, depth = 0): Section { + // the particle curves are edited in the inspector of their emitter, shown here only when they are selected + function getSection(?root : PrefabElement, depth = 0, particles = false): Section { var section: Section = { root:root, curves: [], children: [], events: []}; var eventAdded = false; + particles = particles || (depth == 0 && root is Curve); + function shown(c: Curve) return particles || !c.isParticleCurve(); - if (root is Curve) + if (root is Curve && shown(cast root)) curvesToDraw.push(cast root); if (root is IEvent && depth == 0) eventsToDraw.push(cast root); for(child in root.children) { - if (child.flatten(Curve).length > 0) { + if (child.flatten(Curve).exists(shown)) { if (child is Curve) { var c = Std.downcast(child, Curve); curvesToDraw.push(c); - if (c.blendMode == CurveBlendMode.Blend || c.blendMode == CurveBlendMode.RandomBlend) - section.children.push(getSection(c,depth+1)); - else - section.curves.push(c); + section.curves.push(c); } else { - section.children.push(getSection(child,depth+1)); + section.children.push(getSection(child,depth+1,particles)); } } @@ -1388,13 +1271,16 @@ class FXEditor extends hide.view.FileView { var added = []; for(prop in props) { var id = prefix != null ? prefix + ":" + prop.name : prop.name; - if(Curve.getCurve(element, id) != null) - continue; - var curve = new Curve(element,null); - curve.name = id; - if(prop.def != null) - curve.addKey(0, prop.def, Linear); - added.push(curve); + for(k in hrt.prefab.fx.EmitterParam.animKeys(element, id) ?? [id]) { + if(Curve.getCurve(element, k, false) != null) + continue; + var curve = new Curve(element, null); + curve.name = k; + var v = getAnimValue(element, k) ?? prop.def; + if(v != null) + curve.addKey(0, v, Linear); + added.push(curve); + } } if(added.length == 0) @@ -1422,8 +1308,7 @@ class FXEditor extends hide.view.FileView { var rfxElt = Std.downcast(elt, hrt.prefab.rfx.RendererFX); var screenShaderGraph = Std.downcast(elt, hrt.prefab.rfx.ScreenShaderGraph); - var emitterElt = Std.downcast(elt, hrt.prefab.fx.Emitter); - var emitter2DElt = Std.downcast(elt, hrt.prefab.fx.Emitter2D); + var emitter : hrt.prefab.fx.EmitterParam.IEmitter = elt is hrt.prefab.fx.EmitterParam.IEmitter ? cast elt : null; var particle2dElt = Std.downcast(elt, hrt.prefab.l2d.Particle2D); var menuItems : Array = []; @@ -1436,7 +1321,7 @@ class FXEditor extends hide.view.FileView { function trackItem(name: String, props: Array, ?prefix: String) : hrt.ui.HuiMenu.MenuItem { var hasAllTracks = true; for(p in props) { - if(getTrack(elt, prefix + ":" + p.name) == null) + if(getTrack(elt, prefix != null ? prefix + ":" + p.name : p.name) == null) hasAllTracks = false; } return { @@ -1490,18 +1375,10 @@ class FXEditor extends hide.view.FileView { label: "Position", menu: groupedTracks("position", xyzwTracks(3)), }); - menuItems.push({ - label: "Local Position", - menu: groupedTracks("localPosition", xyzwTracks(3)), - }); menuItems.push({ label: "Rotation", menu: groupedTracks("rotation", xyzwTracks(3)), }); - menuItems.push({ - label: "Local Rotation", - menu: groupedTracks("localRotation", xyzwTracks(3)), - }); menuItems.push({ label: "Scale", menu: scaleTracks, @@ -1564,7 +1441,7 @@ class FXEditor extends hide.view.FileView { } } } - function addParam(param : hrt.prefab.fx.EmitterHelper.ParamDef, prefix: String) { + function addParam(param : hrt.prefab.fx.EmitterHelper.PropertyDef, prefix: String) { var label = prefix + (param.disp != null ? param.disp : upperCase(param.name)); var item : hrt.ui.HuiMenu.MenuItem = switch(param.t) { case PVec(n, _): @@ -1578,30 +1455,12 @@ class FXEditor extends hide.view.FileView { menuItems.push(item); } - if(emitterElt != null) { - for(param in hrt.prefab.fx.Emitter.emitterParams) { - if(!param.animate) - continue; - addParam(param, ""); - } - for(param in hrt.prefab.fx.Emitter.instanceParams) { - if(!param.animate) - continue; - addParam(param, param.instance ? "Instance " : ""); - } - } - - if(emitter2DElt != null) { - for(param in hrt.prefab.fx.Emitter2D.emitterParams) { - if(!param.animate) - continue; - addParam(param, ""); - } - for(param in hrt.prefab.fx.Emitter2D.instanceParams) { - if(!param.animate) - continue; - addParam(param, param.instance ? "Instance " : ""); - } + if(emitter != null) { + for(f in emitter.getFields()) + switch(f) { + case Property(p) if(p.animate): addParam(p, ""); + default: + } } if (particle2dElt != null) { @@ -1684,6 +1543,7 @@ class FXEditor extends hide.view.FileView { onUpdate2D(dt); else onUpdate3D(dt); + sceneEditor.curEdit2?.root?.refreshAnimatedFields(); } function onUpdate2D(dt:Float) { @@ -1850,7 +1710,33 @@ class FXEditor extends hide.view.FileView { } static function getTrack(element : PrefabElement, propName : String) { - return Curve.getCurve(element, propName, false); + var keys = hrt.prefab.fx.EmitterParam.animKeys(element, propName); + return Curve.getCurve(element, keys != null ? keys[0] : propName, false); + } + + /** The current value of the property animated by the curve `key` **/ + static function getAnimValue(element : PrefabElement, key : String) : Null { + inline function num(v: Dynamic) : Null return Std.isOfType(v, Float) || Std.isOfType(v, Int) ? (v : Float) : null; + var parts = key.split(":"); + var isEmitter = element is hrt.prefab.fx.EmitterParam.IEmitter; + var comp = parts.length > 1 ? parts[1] : "x"; + if(element is hrt.prefab.Object3D || element is hrt.prefab.Object2D) { + switch(parts[0]) { + case "position": return num(Reflect.field(element, comp)); + case "rotation" if(element is hrt.prefab.Object2D): return (cast element : hrt.prefab.Object2D).rotation; + case "rotation", "scale": return num(Reflect.field(element, parts[0] + comp.toUpperCase())); + default: + } + } + if(element is hrt.prefab.Shader || element is hrt.prefab.rfx.ScreenShaderGraph) { + var prop : Dynamic = element.props == null ? null : Reflect.field(element.props, parts[0]); + if(Std.isOfType(prop, Array) && parts.length > 1) + prop = (prop : Array)[hrt.prefab.fx.EmitterParam.COMPONENTS.indexOf(parts[1])]; + return num(prop); + } + if(isEmitter || element is hrt.prefab.rfx.RendererFX || (element is Light && parts.length == 1)) + return num(Reflect.field(element, key)); + return null; } static function upperCase(prop: String) { @@ -1858,10 +1744,6 @@ class FXEditor extends hide.view.FileView { return prop.charAt(0).toUpperCase() + prop.substr(1); } - static function isInstanceCurve(curve: Curve) { - return curve.findParent(hrt.prefab.fx.Emitter) != null; - } - static var _ = Extension.registerExtension(FXEditor, ["fx"], { icon : "sitemap", createNew : "FX", name: "FX" }); function set_currentTime(value:Float):Float { diff --git a/hrt/impl/Curve.hx b/hrt/impl/Curve.hx new file mode 100644 index 000000000..31516480d --- /dev/null +++ b/hrt/impl/Curve.hx @@ -0,0 +1,369 @@ +package hrt.impl; + +enum abstract CurveKeyMode(Int) { + var Aligned = 0; + var Free = 1; + var Linear = 2; + var Constant = 3; +} + +@:build(hrt.prefab.Macros.buildSerializable()) +class CurveHandle { + @:s public var dv: Float = -1; // Force serialization of 0 values for retrocompat + @:s public var dt: Float = -1; // Force serialization of 0 values for retrocompat + public function new(t, v) { + this.dt = t; + this.dv = v; + } +} + +@:build(hrt.prefab.Macros.buildSerializable()) +class CurveKey { + @:s public var time: Float = -1; // Force serialization of 0 values for retrocompat + @:s public var value: Float = -1; // Force serialization of 0 values for retrocompat + @:s public var mode: CurveKeyMode = Aligned; + @:s public var prevHandle: CurveHandle; + @:s public var nextHandle: CurveHandle; + public function new() {} +} + +@:build(hrt.prefab.Macros.buildSerializable()) +class Curve { + #if !editor + public static var BAKE_ENABLE = true; + public static var BAKE_MAX_DURATION = 1.0; + public static var BAKE_RESOLUTION = 128.0; + #end + + @:s public var keys : Array = []; + @:s public var loop : Bool = false; + + public var duration(get, never): Float; + function get_duration() { + if (keys.length == 0) return 0.0; + return keys[keys.length-1].time; + } + + #if !editor + @:c var bake : haxe.ds.Vector = null; + @:c var bakeDuration = 0.0; + #end + + public function new() { + } + + public static inline function bezier(c0: Float, c1:Float, c2:Float, c3: Float, t:Float) { + var u = 1 - t; + return u * u * u * c0 + c1 * 3 * t * u * u + c2 * 3 * t * t * u + t * t * t * c3; + } + + public function findKey(time: Float, tolerance: Float) { + var minDist = tolerance; + var closest = null; + for(k in keys) { + var d = hxd.Math.abs(k.time - time); + if(d < minDist) { + minDist = d; + closest = k; + } + } + return closest; + } + + public function addKey(time: Float, ?val: Float, ?mode=null) { + var index = 0; + for(ik in 0...keys.length) { + var key = keys[ik]; + if(time > key.time) + index = ik + 1; + } + + if(val == null) + val = getVal(time); + + var key = new CurveKey(); + key.time = time; + key.value = val; + key.mode = mode ?? keys[index]?.mode ?? Linear; + keys.insert(index, key); + return key; + } + + public function getBounds(bounds: h2d.col.Bounds = null) { + var ret = bounds == null ? new h2d.col.Bounds() : bounds; + + // We always want to show 0,0 + ret.addPos(0.0,0.0); + + for(k in keys) { + ret.addPos(k.time, k.value); + if (k.nextHandle != null) { + ret.addPos(k.time + k.nextHandle.dt, k.value + k.nextHandle.dv); + } + if (k.prevHandle != null) { + ret.addPos(k.time + k.prevHandle.dt, k.value + k.prevHandle.dv); + } + } + + return ret; + } + + #if !editor + public function bakeValues() { + if (bake != null || !BAKE_ENABLE || keys.length < 2 || duration <= 0 || duration > BAKE_MAX_DURATION) + return; + + var numPts = Math.ceil(duration * BAKE_RESOLUTION) + 1; + var samples = new haxe.ds.Vector(numPts); + var prevLoop = loop; + loop = false; // prevent getVal from looping during baking + for (i in 0...numPts) + samples[i] = getVal(i / BAKE_RESOLUTION); + loop = prevLoop; + bake = samples; + bakeDuration = duration; + } + #end + + public function getVal(time: Float) : Float { + #if !editor + if (bake != null) { + var duration = bakeDuration; + var t = time; // HL2 performance hit when modifying func args + if (loop) + t %= duration; + if (t >= 0.0 && t <= duration) { + var fidx = t * BAKE_RESOLUTION; + var idx = Math.floor(fidx); + if (idx >= bake.length - 1) + return bake[bake.length - 1]; + return hxd.Math.lerp(bake[idx], bake[idx + 1], fidx - idx); + } + } + #end + + switch(keys.length) { + case 0: return 0; + case 1: return keys[0].value; + default: + } + + if (loop) + time = time % keys[keys.length-1].time; + + var idx = -1; + for(ik in 0...keys.length) { + var key = keys[ik]; + if(time > key.time) + idx = ik; + } + + if(idx < 0) + return keys[0].value; + + var cur = keys[idx]; + var next = keys[idx + 1]; + if(next == null || cur.mode == Constant) + return cur.value; + + var T = findT(cur, next, time); + var minT = T.minT; + var maxT = T.maxT; + + inline function sampleTime(t) { + return bezier( + cur.time, + cur.time + (cur.nextHandle != null ? cur.nextHandle.dt : 0.), + next.time + (next.prevHandle != null ? next.prevHandle.dt : 0.), + next.time, t); + } + + var x0 = sampleTime(minT); + var x1 = sampleTime(maxT); + var dx = x1 - x0; + var xfactor = dx == 0 ? 0.5 : (time - x0) / dx; + + inline function sampleVal(t) { + return bezier( + cur.value, + cur.value + (cur.nextHandle != null ? cur.nextHandle.dv : 0.), + next.value + (next.prevHandle != null ? next.prevHandle.dv : 0.), + next.value, t); + } + + var y0 = sampleVal(minT); + var y1 = sampleVal(maxT); + var y = y0 + (y1 - y0) * xfactor; + return y; + } + + /** + The lowest and highest values of the curve, exact: its keys, and the extrema of the bezier of each segment (the values + of a segment are a cubic of its parameter, its extrema are where its derivative, a quadratic, is 0). + **/ + public function getValueRange() : { min : Float, max : Float } { + if (keys.length == 0) + return { min: 0., max: 0. }; + var min = keys[0].value, max = min; + inline function add(v: Float) { + if (v < min) min = v; + if (v > max) max = v; + } + for (i in 0...keys.length) { + var cur = keys[i]; + add(cur.value); + var next = keys[i + 1]; + if (next == null || cur.mode == Constant || (cur.nextHandle == null && next.prevHandle == null)) + continue; + var p0 = cur.value, p3 = next.value; + var p1 = p0 + (cur.nextHandle != null ? cur.nextHandle.dv : 0.); + var p2 = p3 + (next.prevHandle != null ? next.prevHandle.dv : 0.); + // B'(s) / 3 = a s^2 + b s + c + var a = -p0 + 3 * p1 - 3 * p2 + p3, b = 2 * (p0 - 2 * p1 + p2), c = p1 - p0; + inline function root(s: Float) if (s > 0 && s < 1) add(bezier(p0, p1, p2, p3, s)); + if (Math.abs(a) < 1e-12) { + if (Math.abs(b) > 1e-12) + root(-c / b); + } else { + var d = b * b - 4 * a * c; + if (d >= 0) { + var q = Math.sqrt(d); + root((-b + q) / (2 * a)); + root((-b - q) / (2 * a)); + } + } + } + return { min: min, max: max }; + } + + public function getSum(time: Float) : Float { + var duration = keys[keys.length-1].time; + if(loop && time > duration) { + var cycles = Math.floor(time / duration); + return getSum(duration) * cycles + getSum(time - cycles); + } + + var sum = 0.0; + for(ik in 0...keys.length) { + var key = keys[ik]; + if(time < key.time) + break; + + if(ik == 0 && key.time > 0) { + // Account for start of curve + sum += key.time * key.value; + } + + var nkey = keys[ik + 1]; + if(nkey != null) { + if(time > nkey.time) { + // Full interval + sum += key.value * (nkey.time - key.time); + if(key.mode != Constant) + sum += 0.5 * (nkey.time - key.time) * (nkey.value - key.value); + } + else { + // Split interval + sum += key.value * (time - key.time); + if(key.mode != Constant) + sum += 0.5 * (time - key.time) * hxd.Math.lerp(key.value, nkey.value, (time - key.time) / (nkey.time - key.time)); + } + } + else { + sum += key.value * (time - key.time); + } + } + return sum; + } + + public function sample(numPts: Int) { + var vals = []; + var duration = this.duration; + for(i in 0...numPts) + vals.push(getVal(duration * i/(numPts-1))); + return vals; + } + + #if castle + public function initFromCDB(curve: cdb.Types.Curve) { + keys = []; + var nbPoints = Std.int(curve.data.length / 6); + for (pointIdx in 0...nbPoints) { + var x = curve.data[pointIdx * 6 + 0]; + var y = curve.data[pointIdx * 6 + 1]; + var prevHandleX = curve.data[pointIdx * 6 + 2]; + var prevHandleY = curve.data[pointIdx * 6 + 3]; + var nextHandleX = curve.data[pointIdx * 6 + 4]; + var nextHandleY = curve.data[pointIdx * 6 + 5]; + + var mode : CurveKeyMode = Free; + + if (Math.isNaN(prevHandleX) || prevHandleX == cdb.Types.Curve.HandleData) { + mode = cast Std.int(prevHandleY); + } + else { + var pt1 = inline new h2d.col.Point(prevHandleX, prevHandleY); + var pt2 = inline new h2d.col.Point(nextHandleX, nextHandleY); + pt1.normalize(); + pt2.normalize(); + var dot = pt1.dot(pt2); + if (Math.abs(dot + 1.0) < 0.01) { + mode = Aligned; + } + } + + var key = addKey(x, y, mode); + if (mode == Free || mode == Aligned) { + key.prevHandle = new CurveHandle(Math.isFinite(prevHandleX) ? prevHandleX : 0.0, Math.isFinite(prevHandleY) ? prevHandleY : 0.0); + key.nextHandle = new CurveHandle(Math.isFinite(nextHandleX) ? nextHandleX : 0.0, Math.isFinite(nextHandleY) ? nextHandleY : 0.0); + } + } + } + + public function toCDB() : cdb.Types.Curve { + var data : Array= []; + for (key in keys) { + data.push(key.time); + data.push(key.value); + + switch(key.mode) { + case Free, Aligned: + data.push(key.prevHandle?.dt ?? 0.0); + data.push(key.prevHandle?.dv ?? 0.0); + data.push(key.nextHandle?.dt ?? 0.0); + data.push(key.nextHandle?.dv ?? 0.0); + case Constant, Linear: + data.push(cdb.Types.Curve.HandleData); + data.push((cast key.mode:Int)); + data.push(cdb.Types.Curve.HandleData); + data.push(cdb.Types.Curve.HandleData); + } + } + return cast data; + } + #end + + inline function findT(cur: CurveKey, next: CurveKey, time: Float) : {minT: Float, maxT: Float} { + inline function sampleTime(t) { + return bezier( + cur.time, + cur.time + (cur.nextHandle != null ? cur.nextHandle.dt : 0.), + next.time + (next.prevHandle != null ? next.prevHandle.dt : 0.), + next.time, t); + } + + var minT = 0.; + var maxT = 1.; + var maxDelta = 1./ 25.; + + while( maxT - minT > maxDelta ) { + var t = (maxT + minT) * 0.5; + var x = sampleTime(t); + if( x > time ) + maxT = t; + else + minT = t; + } + return {minT: minT, maxT: maxT}; + } +} diff --git a/hrt/impl/Gradient.hx b/hrt/impl/Gradient.hx index 11a4b70ec..b7ff20ffa 100644 --- a/hrt/impl/Gradient.hx +++ b/hrt/impl/Gradient.hx @@ -246,12 +246,14 @@ class Gradient { texture.resize(pixels.width, pixels.height); } texture.uploadPixels(pixels); + texture.filter = data.interpolation == Constant ? Nearest : Linear; return texture; } #end var texture = Texture.fromPixels(genPixels(), RGBA); + texture.filter = data.interpolation == Constant ? Nearest : Linear; texture.realloc = function() { texture.uploadPixels(genPixels()); } diff --git a/hrt/prefab/Curve.hx b/hrt/prefab/Curve.hx index df6fa7bda..319d4dd4b 100644 --- a/hrt/prefab/Curve.hx +++ b/hrt/prefab/Curve.hx @@ -5,620 +5,84 @@ import hide.prefab.EditContext; import hide.prefab.HideProps; #end - -import hrt.prefab.fx.Value; -import hrt.prefab.fx.Evaluator; +import hrt.impl.Curve.CurveKey; using Lambda; -@:build(hrt.prefab.Macros.buildSerializable()) -class CurveHandle { - @:s public var dv: Float = -1; // Force serialization of 0 values for retrocompat - @:s public var dt: Float = -1; // Force serialization of 0 values for retrocompat - public function new(t, v) { - this.dt = t; - this.dv = v; - } -} - -enum abstract CurveKeyMode(Int) { - var Aligned = 0; - var Free = 1; - var Linear = 2; - var Constant = 3; -} - -enum abstract CurveBlendMode(Int) { - var None = 0; - var Blend = 1; - var RandomBlend = 2; - var Reference = 3; -} - -@:build(hrt.prefab.Macros.buildSerializable()) -class CurveKey { - @:s public var time: Float = -1; // Force serialization of 0 values for retrocompat - @:s public var value: Float = -1; // Force serialization of 0 values for retrocompat - @:s public var mode: CurveKeyMode = Aligned; - @:s public var prevHandle: CurveHandle; - @:s public var nextHandle: CurveHandle; - public function new() {} -} - -typedef CurveKeys = Array; - @:prefabIcon(HuiRes.ui.icons.prefab.curve) class Curve extends Prefab { - #if !editor - public static var BAKE_ENABLE = true; - public static var BAKE_MAX_DURATION = 1.0; - public static var BAKE_RESOLUTION = 128.0; - #end - - @:s public var keyMode : CurveKeyMode = Linear; - @:s public var keys : CurveKeys = []; - @:s public var previewKeys : CurveKeys = []; + @:isVar public var data(get, null) : hrt.impl.Curve; + function get_data() { + if (data == null) + data = new hrt.impl.Curve(); + return data; + } - @:s public var blendMode : CurveBlendMode = None; - @:s public var loop : Bool = false; - @:s public var blendParam : String = null; - @:s public var remapPath : String = null; + public var keys(get, set) : Array; + function get_keys() return data.keys; + function set_keys(v) return data.keys = v; - @:s public var offset : Float = 0.0; - @:s public var scale : Float = 1.0; + public var loop(get, set) : Bool; + function get_loop() return data.loop; + function set_loop(v) return data.loop = v; - public var maxTime : Float = 5000.; public var duration(get, never): Float; - public var minValue : Float = 0.; - public var maxValue : Float = 0.; - - public var color : Int; - public var hidden : Bool = false; - public var lock : Bool = false; - public var selected : Bool = false; - - var refCurve : Curve = null; - - #if !editor - @:c var bake : haxe.ds.Vector = null; - @:c var bakeDuration = 0.0; - #end - - function get_duration() { - if (blendMode == Reference) { - return getRef()?.duration ?? 0.0; - } - if(keys.length == 0) return 0.0; - if (blendMode == CurveBlendMode.Blend || blendMode == CurveBlendMode.RandomBlend) { - var c1:Curve = cast this.children[0]; - var c2:Curve = cast this.children[1]; - return Math.min(c1.duration, c2.duration); - } - return keys[keys.length-1].time; - } + function get_duration() return data.duration; public function new(parent, shared: ContextShared) { super(parent, shared); } public override function load(o:Dynamic) { - if (o.blendMode != null && !Std.isOfType(o.blendMode, Int)) { - o.blendMode = Std.parseInt(o.blendMode); - } super.load(o); + loadData(o); if( keys.length == 0 ) { addKey(0.0, 0.0); addKey(1.0, 1.0); } name = StringTools.replace(name, ".", ":"); - #if editor - if (Std.downcast(parent, Curve) != null) { - if (StringTools.startsWith(name, parent.name)) { - name = StringTools.replace(name, parent.name + ":", ""); - } - } - #else + #if !editor bakeValues(); #end } - override function copy(o:Prefab) { - super.copy(o); - } - - override function makeInstance() { - refCurve = null; + function loadData(o:Dynamic) { + @:privateAccess data.copyFromDynamic(o); } - static inline function bezier(c0: Float, c1:Float, c2:Float, c3: Float, t:Float) { - var u = 1 - t; - return u * u * u * c0 + c1 * 3 * t * u * u + c2 * 3 * t * t * u + t * t * t * c3; + function saveData(o:Dynamic) { + @:privateAccess data.copyToDynamic(o); } public function findKey(time: Float, tolerance: Float) { - var minDist = tolerance; - var closest = null; - for(k in keys) { - var d = hxd.Math.abs(k.time - time); - if(d < minDist) { - minDist = d; - closest = k; - } - } - return closest; + return data.findKey(time, tolerance); } public function addKey(time: Float, ?val: Float, ?mode=null) { - var index = 0; - for(ik in 0...keys.length) { - var key = keys[ik]; - if(time > key.time) - index = ik + 1; - } - - if(val == null) - val = getVal(time); - - var key = new hrt.prefab.Curve.CurveKey(); - key.time = time; - key.value = val; - key.mode = mode != null ? mode : (keys[index] != null ? keys[index].mode : keyMode); - keys.insert(index, key); - return key; - } - - public function addPreviewKey(time: Float, val: Float) { - var key = new hrt.prefab.Curve.CurveKey(); - key.time = time; - key.value = val; - previewKeys.push(key); - return key; + return data.addKey(time, val, mode); } public function getBounds(bounds: h2d.col.Bounds = null) { - if (blendMode == Reference) { - return getRef()?.getBounds(bounds) ?? bounds ?? new h2d.col.Bounds(); - } - var ret = bounds == null ? new h2d.col.Bounds() : bounds; - - // We always want to show 0,0 - ret.addPos(0.0,0.0); - - for(k in keys) { - ret.addPos(k.time, k.value); - if (k.nextHandle != null) { - ret.addPos(k.time + k.nextHandle.dt, k.value + k.nextHandle.dv); - } - if (k.prevHandle != null) { - ret.addPos(k.time + k.prevHandle.dt, k.value + k.prevHandle.dv); - } - } - - return ret; - } - - function getRef() : Null { - if (refCurve != null) - return refCurve; - var root = getRoot(false); - refCurve = Std.downcast(root.locatePrefab(blendParam), Curve); - if (refCurve == this) refCurve = null; - return refCurve; - } - - public function getRemapParameter() : Null { - if (remapPath == null) - return null; - var param = StringTools.replace(remapPath, "$param.", ""); - if (param != remapPath) - return param; - return null; - } - - public function makeVal(ignoreRemap: Bool = false) : Value { - if (!ignoreRemap) { - if (remapPath == null) { - return makeVal(true); - } - var val = makeVal(true); - var param = getRemapParameter(); - if (param != null) { - return VParamRemap(val, param); - } - else { - var root = getRoot(false); - var remapCurve = Std.downcast(root.locatePrefab(remapPath), Curve); - return remapCurve != null ? VValueRemap(val, remapCurve.makeVal()) : val; - } - } - - switch(blendMode) { - case None: - return VCurve(this); - case Blend: - var ca = Std.downcast(this.children[0], Curve); - var cb = Std.downcast(this.children[1], Curve); - if (ca == null) return cb != null ? VCurve(cb) : VConst(0.0); - if (cb == null) return VCurve(ca); - return VBlendCurves(1.0, 0.0, ca, cb, blendParam); - case RandomBlend: - return VCurve(this); - case Reference: - var val = getRef()?.makeVal(); - if (val == null) - return VConst(0.0); - if (scale != 1.0) - val = Evaluator.vMult(val, VConst(scale)); - if (offset != 0.0) - val = Evaluator.vAdd(val, VConst(offset)); - return val; - default: - throw "invalid blendMode value"; - } - } - - #if !editor - function bakeValues() { - if (bake != null || !BAKE_ENABLE || blendMode != None || keys.length < 2 || duration <= 0 || duration > BAKE_MAX_DURATION) - return; - - var numPts = Math.ceil(duration * BAKE_RESOLUTION) + 1; - var samples = new haxe.ds.Vector(numPts); - var prevLoop = loop; - loop = false; // prevent getVal from looping during baking - for (i in 0...numPts) - samples[i] = getVal(i / BAKE_RESOLUTION); - loop = prevLoop; - bake = samples; - bakeDuration = duration; - } - #end - - inline function findT(cur: CurveKey, next: CurveKey, time: Float) : {minT: Float, maxT: Float} { - inline function sampleTime(t) { - return bezier( - cur.time, - cur.time + (cur.nextHandle != null ? cur.nextHandle.dt : 0.), - next.time + (next.prevHandle != null ? next.prevHandle.dt : 0.), - next.time, t); - } - - var minT = 0.; - var maxT = 1.; - var maxDelta = 1./ 25.; - - while( maxT - minT > maxDelta ) { - var t = (maxT + minT) * 0.5; - var x = sampleTime(t); - if( x > time ) - maxT = t; - else - minT = t; - } - return {minT: minT, maxT: maxT}; + return data.getBounds(bounds); } public function getVal(time: Float) : Float { - #if !editor - if (bake != null) { - var duration = bakeDuration; - var t = time; // HL2 performance hit when modifying func args - if (loop) - t %= duration; - if (t >= 0.0 && t <= duration) { - var fidx = t * BAKE_RESOLUTION; - var idx = Math.floor(fidx); - if (idx >= bake.length - 1) - return bake[bake.length - 1]; - return hxd.Math.lerp(bake[idx], bake[idx + 1], fidx - idx); - } - } - #end - - if (blendMode == Reference) { - throw "getVal shoudln't be called on curves with Reference mode"; - } - - switch(keys.length) { - case 0: return 0; - case 1: return keys[0].value; - default: - } - - if (loop) - time = time % keys[keys.length-1].time; - - var idx = -1; - for(ik in 0...keys.length) { - var key = keys[ik]; - if(time > key.time) - idx = ik; - } - - if(idx < 0) - return keys[0].value; - - var cur = keys[idx]; - var next = keys[idx + 1]; - if(next == null || cur.mode == Constant) - return cur.value; - - var T = findT(cur, next, time); - var minT = T.minT; - var maxT = T.maxT; - - inline function sampleTime(t) { - return bezier( - cur.time, - cur.time + (cur.nextHandle != null ? cur.nextHandle.dt : 0.), - next.time + (next.prevHandle != null ? next.prevHandle.dt : 0.), - next.time, t); - } - - var x0 = sampleTime(minT); - var x1 = sampleTime(maxT); - var dx = x1 - x0; - var xfactor = dx == 0 ? 0.5 : (time - x0) / dx; - - inline function sampleVal(t) { - return bezier( - cur.value, - cur.value + (cur.nextHandle != null ? cur.nextHandle.dv : 0.), - next.value + (next.prevHandle != null ? next.prevHandle.dv : 0.), - next.value, t); - } - - var y0 = sampleVal(minT); - var y1 = sampleVal(maxT); - var y = y0 + (y1 - y0) * xfactor; - return y; + return data.getVal(time); } public function getSum(time: Float) : Float { - if (blendMode == Reference) { - return getRef()?.getSum(time) ?? 0.0; - } - var duration = keys[keys.length-1].time; - if(loop && time > duration) { - var cycles = Math.floor(time / duration); - return getSum(duration) * cycles + getSum(time - cycles); - } - - var sum = 0.0; - for(ik in 0...keys.length) { - var key = keys[ik]; - if(time < key.time) - break; - - if(ik == 0 && key.time > 0) { - // Account for start of curve - sum += key.time * key.value; - } - - var nkey = keys[ik + 1]; - if(nkey != null) { - if(time > nkey.time) { - // Full interval - sum += key.value * (nkey.time - key.time); - if(key.mode != Constant) - sum += 0.5 * (nkey.time - key.time) * (nkey.value - key.value); - } - else { - // Split interval - sum += key.value * (time - key.time); - if(key.mode != Constant) - sum += 0.5 * (time - key.time) * hxd.Math.lerp(key.value, nkey.value, (time - key.time) / (nkey.time - key.time)); - } - } - else { - sum += key.value * (time - key.time); - } - } - return sum; + return data.getSum(time); } public function sample(numPts: Int) { - if (blendMode == Reference) { - return getRef()?.sample(numPts) ?? []; - } - var vals = []; - - var duration = this.duration; - for(i in 0...numPts) { - var v = 0.0; - v = getVal(duration * i/(numPts-1)); - vals.push(v); - if (v < minValue) minValue = v; - if (v > maxValue) maxValue = v; - } - return vals; + return data.sample(numPts); } - override function edit2(ctx: hrt.prefab.EditContext2) { - super.edit2(ctx); - - ctx.build( - - - - -
Blend Mode
- -
-
-
Parameter
- -
-
-
-
Curve Path
- -
-
Offset
-
Scale
-
-
-
Time Remap
- -
-
- -
'); - - var blendModeSelect = props.find('#blendMode'); - - var refreshBlend = function() { - var parameter = props.find('#parameter'); - if (blendMode != Blend) { - parameter.hide(); - } - else { - var selecta = parameter.find('[field="blendParam"]'); - parameter.show(); - selecta.empty(); - var root = Std.downcast(getRoot(false), hrt.prefab.fx.FX); - if (root != null) { - for (p in root.parameters) { - selecta.append(new hide.Element('')); - } - selecta.val(blendParam); - } - } - - var reference = props.find('#reference'); - if (blendMode != Reference) { - reference.hide(); - } - else { - var selecta = reference.find('[field="blendParam"]'); - reference.show(); - selecta.empty(); - var root = getRoot(false); - var flat = root.flatten(Curve); - for (p in flat) { - if (p == this) continue; - var path = p.getAbsPath(); - selecta.append(new hide.Element('')); - } - selecta.val(blendParam); - } - - { - var select = props.find('[field=remapPath]'); - select.empty(); - var root = getRoot(false); - select.append(new hide.Element('')); - var flat = root.flatten(Curve); - var parameters = Std.downcast(root, hrt.prefab.fx.FX)?.parameters ?? []; - for (param in parameters) { - var path = "$param." + param.name; - select.append(new hide.Element('')); - } - - for (p in flat) { - if (p == this) continue; - var path = p.getAbsPath(); - select.append(new hide.Element('')); - } - select.val(remapPath); - } - } - - refreshBlend(); - - blendModeSelect.val(Std.string(blendMode)); - blendModeSelect.change((_) -> { - var oldBlend = this.blendMode; - var oldParam = this.blendParam; - var newBlend = cast Std.parseInt(blendModeSelect.val()); - var newParam = null; - - function exec(undo : Bool) { - blendMode = undo ? oldBlend : newBlend; - blendParam = undo ? oldParam : newParam; - refreshBlend(); - ctx.onChange(this, "blendMode"); - ctx.rebuildPrefab(this); - } - - exec(false); - ctx.properties.undo.change(Custom(exec)); - }); - - ctx.properties.add(props, this, function(pname) { - if (pname == "blendParam") { - refCurve = null; - if (containsRefCycle()) { - hide.Ide.inst.quickMessage("Curve reference create a cycle"); - blendParam = null; - refCurve = null; - } - } - if (pname == "remapPath") { - if (remapPath == "") - remapPath = null; - } - refreshBlend(); - @:privateAccess ctx.scene.editor.refreshTree(SceneTree); - ctx.onChange(this, pname); - }); - - var ce = new hide.comp.CurveEditor(ctx.properties.undo, ctx.properties.element, false); - ce.saveDisplayKey = this.getAbsPath(true); - ce.curves.push(this); - ce.refresh(); - } - - override function getHideProps() : HideProps { - return { icon : "paint-brush", name : "Curve" }; - } - // Returns the visual representation of this curve as an svg path `d` data public function getSvgString() : String { var data = ""; @@ -650,7 +114,7 @@ class Curve extends Prefab { var r0: Float; var r1: Float; - var midt = bezier(c0,c1,c2,c3, 0.5); + var midt = hrt.impl.Curve.bezier(c0,c1,c2,c3, 0.5); if (midt < c0) { // solve bezier(c0,c1,c2,c2, x) = c0 for x; var div = 2*c3-6*c2+6*c1-2*c0; @@ -707,7 +171,7 @@ class Curve extends Prefab { r1 = hxd.Math.clamp(r1, 0.0, 1.0); inline function b(t) { - bezier(c0,c1,c2,c3, t); + hrt.impl.Curve.bezier(c0,c1,c2,c3, t); } // split the curve in two if its loops on itself @@ -747,49 +211,30 @@ class Curve extends Prefab { } #end - function containsRefCycle(?visited: Map) : Bool { - if (blendMode != Reference) return false; - var visited = visited?.copy() ?? []; - var cur = this; - while (cur != null) { - if (visited.get(cur) != null) - { - return true; - } - visited.set(cur, true); - if (cur.blendMode == Blend || cur.blendMode == RandomBlend) { - for (i in 0...2) { - var subCurve = Std.downcast(cur.children[i], Curve); - if (subCurve.containsRefCycle(visited)) { - return true; - } - } - } - cur = cur.getRef(); - } - return false; + public static function getCurve(parent : Prefab, name: String, onlyEnabled=true) : Curve { + return findCurve(parent, name, onlyEnabled, c -> c.to(Curve)); } - static inline function checkName(a: String, b: String) { - return a == b || a.toLowerCase() == b.toLowerCase(); + public static function getCurves(parent: Prefab, prefix: String) : Array { + return findCurves(parent, prefix, c -> c.to(Curve)); } - public static function getCurve(parent : Prefab, name: String, onlyEnabled=true) { + public static function findCurve(parent : Prefab, name: String, onlyEnabled: Bool, get: Prefab -> Null) : T { for(c in parent.children) { if(onlyEnabled && !c.enabled) continue; #if editor if (parent.shared.editor?.isHidden(c)) continue; #end if(!checkName(c.name, name)) continue; - var curve = c.to(Curve); + var curve = get(c); if(curve == null) continue; return curve; } return null; } - public static function getCurves(parent: Prefab, prefix: String) { - var ret = null; + public static function findCurves(parent: Prefab, prefix: String, get: Prefab -> Null) : Array { + var ret : Array = null; for(c in parent.children) { if(!c.enabled) continue; #if editor @@ -799,7 +244,7 @@ class Curve extends Prefab { var curvePrefix = (idx >= 0) ? c.name.substr(0, idx) : c.name; if(!checkName(curvePrefix, prefix)) continue; - var curve = c.to(Curve); + var curve = get(c); if(curve == null) continue; if (ret == null) ret = []; ret.push(curve); @@ -826,127 +271,87 @@ class Curve extends Prefab { } #if castle - public function initFromCDB(curve: cdb.Types.Curve) { - keys = []; - var nbPoints = Std.int(curve.data.length / 6); - for (pointIdx in 0...nbPoints) { - var x = curve.data[pointIdx * 6 + 0]; - var y = curve.data[pointIdx * 6 + 1]; - var prevHandleX = curve.data[pointIdx * 6 + 2]; - var prevHandleY = curve.data[pointIdx * 6 + 3]; - var nextHandleX = curve.data[pointIdx * 6 + 4]; - var nextHandleY = curve.data[pointIdx * 6 + 5]; - - var mode : hrt.prefab.Curve.CurveKeyMode = Free; - - if (Math.isNaN(prevHandleX) || prevHandleX == cdb.Types.Curve.HandleData) { - mode = cast Std.int(prevHandleY); - } - else { - var pt1 = inline new h2d.col.Point(prevHandleX, prevHandleY); - var pt2 = inline new h2d.col.Point(nextHandleX, nextHandleY); - pt1.normalize(); - pt2.normalize(); - var dot = pt1.dot(pt2); - if (Math.abs(dot + 1.0) < 0.01) { - mode = Aligned; - } - } - - var key = addKey(x, y, mode); - if (mode == Free || mode == Aligned) { - key.prevHandle = new hrt.prefab.Curve.CurveHandle(Math.isFinite(prevHandleX) ? prevHandleX : 0.0, Math.isFinite(prevHandleY) ? prevHandleY : 0.0); - key.nextHandle = new hrt.prefab.Curve.CurveHandle(Math.isFinite(nextHandleX) ? nextHandleX : 0.0, Math.isFinite(nextHandleY) ? nextHandleY : 0.0); - } - } + public function initFromCDB(c: cdb.Types.Curve) { + data.initFromCDB(c); } public function toCDB() : cdb.Types.Curve { - var data : Array= []; - for (key in keys) { - data.push(key.time); - data.push(key.value); - - switch(key.mode) { - case Free, Aligned: - data.push(key.prevHandle?.dt ?? 0.0); - data.push(key.prevHandle?.dv ?? 0.0); - data.push(key.nextHandle?.dt ?? 0.0); - data.push(key.nextHandle?.dv ?? 0.0); - case Constant, Linear: - data.push(cdb.Types.Curve.HandleData); - data.push((cast key.mode:Int)); - data.push(cdb.Types.Curve.HandleData); - data.push(cdb.Types.Curve.HandleData); - } - } - return cast data; + return data.toCDB(); } #end - static inline function findCurve(curves: Array, suffix: String) { - return curves.find(c -> StringTools.endsWith(c.name.toLowerCase(), suffix)); + #if !editor + function bakeValues() { + data.bakeValues(); } + #end - public static function getVectorValue(curves: Array, defVal: Float=0.0, scale: Float=1.0, randomValue: Float = 0) : Value { - inline function find(s) { - return findCurve(curves, s); - } - var x = find(":x"); - var y = find(":y"); - var z = find(":z"); - var w = find(":w"); - - inline function curveOrVal(c: Curve, defVal: Float) : Value { - if (c == null) - return VConst(defVal); - - var vc = c.makeVal(); - if (scale != 1.0) { - return VMult(vc, VConst(scale)); - } - return vc; - } + override function save() { + var o = super.save(); + saveData(o); + return o; + } - return VVector( - curveOrVal(x, defVal), - curveOrVal(y, defVal), - curveOrVal(z, defVal), - curveOrVal(w, 1.0)); + override function copy(o:Prefab) { + super.copy(o); + copyData(cast o); } - public static function getColorValue(curves: Array) : Value { - inline function find(s) { - return findCurve(curves, s); - } + function copyData(o:Curve) { + #if (editor || editor_hl) + data = new hrt.impl.Curve(); + @:privateAccess data.copyFromOther(o.data); + #else + // Optim: in game consider curves immutable to save allocs on FX play + data = o.data; + #end + } - var r = find(":r"); - var g = find(":g"); - var b = find(":b"); - var a = find(":a"); - var h = find(":h"); - var s = find(":s"); - var l = find(":l"); - - if(h != null || s != null || l != null) { - return VHsl( - h != null ? h.makeVal() : VConst(0.0), - s != null ? s.makeVal() : VConst(1.0), - l != null ? l.makeVal() : VConst(1.0), - a != null ? a.makeVal() : VConst(1.0)); + override function edit2(ctx: hrt.prefab.EditContext2) { + super.edit2(ctx); + + ctx.build( + + + + + + + ); + + if (curveEditor != null) { + curveEditor.maxTime = Math.POSITIVE_INFINITY; + curveEditor.addTrack(data); } + } + + #if editor + override function edit( ctx : EditContext ) { + super.edit(ctx); + + var props = new hide.Element(' +
+
+
Loop curve
+
+
'); + ctx.properties.add(props, this, function(pname) { + ctx.onChange(this, pname); + }); - if(a != null && r == null && g == null && b == null) - return a.makeVal(); + var ce = new hide.comp.Timeline(ctx.properties.undo, ctx.properties.element, false); + ce.saveDisplayKey = this.getAbsPath(true); + ce.curves.push(this); + ce.refresh(); + } - if(a == null && r == null && g == null && b == null) - return VConst(1.0); // White by default + override function getHideProps() : HideProps { + return { icon : "paint-brush", name : "Curve", allowParent : p -> !hrt.prefab.fx.FX.inFX(p), allowChildren : _ -> false }; + } + #end - return VVector( - r != null ? r.makeVal() : VConst(1.0), - g != null ? g.makeVal() : VConst(1.0), - b != null ? b.makeVal() : VConst(1.0), - a != null ? a.makeVal() : VConst(1.0)); + static inline function checkName(a: String, b: String) { + return a == b || a.toLowerCase() == b.toLowerCase(); } static var _ = Prefab.register("curve", Curve); diff --git a/hrt/prefab/EditContext2.hx b/hrt/prefab/EditContext2.hx index 27ac3f0c3..1dacb6eac 100644 --- a/hrt/prefab/EditContext2.hx +++ b/hrt/prefab/EditContext2.hx @@ -162,6 +162,40 @@ abstract class EditContext2 { recordUndoImpl(callback, prefabs); } + /** Applies `f` and records the change of `p` (its data and its list of children) as an undo step **/ + public function recordPrefabUndoWithChildren(p: Prefab, f: () -> Void) { + var before = Diff.deepCopy(@:privateAccess p.save()); + var childrenBefore = p.children.copy(); + f(); + var after = Diff.deepCopy(@:privateAccess p.save()); + var childrenAfter = p.children.copy(); + function apply(isUndo: Bool) { + var children = isUndo ? childrenBefore : childrenAfter; + for (c in p.children.copy()) + if (!children.contains(c)) + c.remove(); + for (i => c in children) + if (c.parent != p) + p.addChildAt(c, i); + tempLoad(p, Diff.deepCopy(isUndo ? before : after)); + rebuildTree(p); + rebuildPrefab(p); + rebuildInspector(); + } + rebuildTree(p); + rebuildPrefab(p); + recordUndo(apply, [p]); + } + + /** Loads `data`, a copy of the `save()` of `p`, as an undo step does **/ + public static function tempLoad(p: Prefab, data: Dynamic) { + #if (editor || editor_hl) + @:privateAccess p.editorTempLoad(data); + #else + @:privateAccess p.load(data); + #end + } + /** Implement this to record the undo step in the editor undo stack **/ diff --git a/hrt/prefab/Light.hx b/hrt/prefab/Light.hx index 8533041ae..e325ca89e 100644 --- a/hrt/prefab/Light.hx +++ b/hrt/prefab/Light.hx @@ -405,7 +405,7 @@ class Light extends Object3D { - + @@ -415,14 +415,14 @@ class Light extends Object3D { - - - + + + - - + + diff --git a/hrt/prefab/Object3D.hx b/hrt/prefab/Object3D.hx index 8bf236b46..c5130460e 100644 --- a/hrt/prefab/Object3D.hx +++ b/hrt/prefab/Object3D.hx @@ -285,19 +285,19 @@ class Object3D extends Prefab { ctx.build( - - - + + + - - - + + + - - - + + + , this diff --git a/hrt/prefab/Props.hx b/hrt/prefab/Props.hx index 40feeb7ec..6c5c1bc5b 100644 --- a/hrt/prefab/Props.hx +++ b/hrt/prefab/Props.hx @@ -20,5 +20,6 @@ typedef PropDef = { name : String, t : PropType, ?def: Dynamic, - ?disp: String + ?disp: String, + ?desc: String }; diff --git a/hrt/prefab/Shader.hx b/hrt/prefab/Shader.hx index b3208b657..6684c6233 100644 --- a/hrt/prefab/Shader.hx +++ b/hrt/prefab/Shader.hx @@ -2,6 +2,7 @@ package hrt.prefab; import hrt.impl.Gradient; import hrt.impl.TextureType; +import hrt.prefab.fx.EmitterParam; #if editor import hide.prefab.HideProps; @@ -19,11 +20,32 @@ class Shader extends Prefab { public var shader : hxsl.Shader; + // When under an emitter, shader params can use modes + public var particleParams : Map; + function new(parent, sh: ContextShared) { super(parent, sh); props = {}; } + override function load(data: Dynamic) { + super.load(data); + particleParams = EmitterParam.loadShaderModes(data.particleParams); + } + + override function save() : Dynamic { + var data = super.save(); + var pp = EmitterParam.saveShaderModes(particleParams); + if (pp != null) + data.particleParams = pp; + return data; + } + + override function copy(data: Prefab) { + super.copy(data); + particleParams = (cast data : Shader).particleParams?.copy(); + } + public function makeShader() : hxsl.Shader { return null; } @@ -293,7 +315,8 @@ class Shader extends Prefab { ctx.build( ); - shaderCat.buildPropsList(props, this.props); + if (!hrt.prefab.fx.EmitterHelper.editParticleShader(ctx, this, props, shaderCat)) + shaderCat.buildPropsList(props, this.props); } function getEditProps(shaderDef: hxsl.SharedShader) : Array { diff --git a/hrt/prefab/fx/BaseFX.hx b/hrt/prefab/fx/BaseFX.hx index 46dabd361..9bf987b07 100644 --- a/hrt/prefab/fx/BaseFX.hx +++ b/hrt/prefab/fx/BaseFX.hx @@ -1,5 +1,4 @@ package hrt.prefab.fx; -import hrt.prefab.Curve; import hrt.prefab.Prefab as PrefabElement; import hrt.prefab.fx.Value as Value; @@ -153,16 +152,17 @@ class ShaderDynAnimation extends ShaderAnimation { } } +// Supports overriding xyz components separately +typedef VectorAnimation = { ?x: Value, ?y: Value, ?z: Value }; + typedef ObjectAnimation = { ?elt: hrt.prefab.Object3D, ?obj: h3d.scene.Object, ?elt2d: hrt.prefab.Object2D, ?obj2d: h2d.Object, - ?position: Value, - ?localPosition: Value, - ?scale: Value, - ?rotation: Value, - ?localRotation: Value, + ?position: VectorAnimation, + ?scale: VectorAnimation, + ?rotation: VectorAnimation, ?color: Value, ?visibility: Value, ?additionalProperies : AdditionalProperies @@ -184,7 +184,7 @@ interface BaseFX { class BaseFXTools { public static var useAutoPerInstance = true; - public static function makeShaderParams(basePrefab: hrt.prefab.Prefab, shaderDef: hxsl.SharedShader) { + public static function makeShaderParams(basePrefab: hrt.prefab.Prefab, shaderDef: hxsl.SharedShader, particle = false) { if(shaderDef == null) return null; @@ -197,45 +197,103 @@ class BaseFXTools { paramCount++; - var prop = Reflect.field(basePrefab.props, v.name); - if (prop == null) - prop = Reflect.field(basePrefab, v.name); - if (prop == null) - prop = hrt.prefab.DynamicShader.getDefault(v.type); - - var curves = Curve.getCurves(basePrefab, v.name); - if(curves == null || curves.length == 0) + var size = particle ? EmitterParam.getParamSize(v) : 0; + var val = size > 0 ? makeParticleShaderValue(cast basePrefab, v.name, size) : makeShaderParamValue(basePrefab, v.name, v.type); + if(val == null) continue; + if(ret == null) ret = []; + ret.push({ + idx: paramCount - 1, + def: v, + value: val, + }); + } - switch(v.type) { - case TVec(_, VFloat) : - var isColor = v.name.toLowerCase().indexOf("color") >= 0; - var val = isColor ? Curve.getColorValue(curves) : Curve.getVectorValue(curves); - if(ret == null) ret = []; - ret.push({ - idx: paramCount - 1, - def: v, - value: val, - }); + return ret; + } - default: - var base = 1.0; - if(Std.isOfType(prop, Float) || Std.isOfType(prop, Int)) - base = cast prop; - var curve = Curve.getCurve(basePrefab, v.name); - var val = Value.VConst(base); - if(curve != null) - val = Value.VMult(curve.makeVal(), VConst(base)); - if(ret == null) ret = []; - ret.push({ - idx: paramCount - 1, - def: v, - value: val, - }); - } + public static function getVectorValue(curves: Array, defVal: Float=0.0) : Value { + inline function find(s) { + return findSuffix(curves, s); } + var x = find(":x"); + var y = find(":y"); + var z = find(":z"); - return ret; + inline function curveOrVal(c: Curve, defVal: Float) : Value { + return c == null ? VConst(defVal) : c.makeVal(); + } + + return VVector( + curveOrVal(x, defVal), + curveOrVal(y, defVal), + curveOrVal(z, defVal)); + } + + public static function getColorValue(curves: Array) : Value { + inline function find(s) { + return findSuffix(curves, s); + } + + var r = find(":r"); + var g = find(":g"); + var b = find(":b"); + var a = find(":a"); + var h = find(":h"); + var s = find(":s"); + var l = find(":l"); + + if(h != null || s != null || l != null) { + return VHsl( + h != null ? h.makeVal() : VConst(0.0), + s != null ? s.makeVal() : VConst(1.0), + l != null ? l.makeVal() : VConst(1.0), + a != null ? a.makeVal() : VConst(1.0)); + } + + if(a != null && r == null && g == null && b == null) + return a.makeVal(); + + if(a == null && r == null && g == null && b == null) + return VConst(1.0); // White by default + + return VColor( + r != null ? r.makeVal() : VConst(1.0), + g != null ? g.makeVal() : VConst(1.0), + b != null ? b.makeVal() : VConst(1.0), + a != null ? a.makeVal() : VConst(1.0)); + } + + static inline function findSuffix(curves: Array, suffix: String) { + return Lambda.find(curves, c -> StringTools.endsWith(c.name.toLowerCase(), suffix)); + } + + public static function makeShaderParamValue(basePrefab: hrt.prefab.Prefab, name: String, type: hxsl.Ast.Type) : Value { + var curves = Curve.getCurves(basePrefab, name); + if(curves == null || curves.length == 0) + return null; + switch(type) { + case TVec(_, VFloat): + return name.toLowerCase().indexOf("color") >= 0 ? getColorValue(curves) : getVectorValue(curves); + default: + return Curve.getCurve(basePrefab, name)?.makeVal(); + } + } + + public static function makeParticleShaderValue(shader: hrt.prefab.Shader, name: String, size: Int) : Value { + if(shader.particleParams == null) + return null; + var keys = [for(i in 0...size) EmitterParam.curveName(name, size, i)]; + inline function isCurve(i: Int) return switch(shader.particleParams.get(keys[i])) { case Curve: true; default: false; }; + if(![for(i in 0...size) isCurve(i)].contains(true)) + return null; + var prop : Dynamic = Reflect.field(shader.props, name); + function comp(i: Int) { + var base = EmitterParam.baseComponent(prop, size, i); + return isCurve(i) ? EmitterParam.ParamBuilder.curve(shader, keys[i], base) : Evaluator.vVal(base); + } + // 4 components: a color + return size == 1 ? comp(0) : size > 3 ? VColor(comp(0), comp(1), comp(2), comp(3)) : VVector(comp(0), comp(1), size > 2 ? comp(2) : VZero); } public static function makeRendererFXParams(rfxElt: hrt.prefab.rfx.RendererFX) { @@ -247,25 +305,31 @@ class BaseFXTools { if (!(Reflect.field(rfxElt, f.name) is Float)) continue; - var curves = Curve.getCurves(rfxElt, f.name); - if(curves == null || curves.length == 0) - continue; - - var base = 1.0; var curve = Curve.getCurve(rfxElt, f.name); - var val = Value.VConst(base); - if(curve != null) - val = Value.VMult(curve.makeVal(), VConst(base)); + if(curve == null) + continue; if(ret == null) ret = []; ret.push({ field : f, - value : val + value : curve.makeVal() }); } return ret; } + public static function vectorAnimation(elt: PrefabElement, name: String) : VectorAnimation { + var curves = Curve.getCurves(elt, name); + if(curves == null || curves.length == 0) + return null; + if(curves.length == 1 && curves[0].name == name) { + var v = curves[0].makeVal(); + return { x: v, y: v, z: v }; + } + inline function comp(suffix: String) return Lambda.find(curves, c -> StringTools.endsWith(c.name.toLowerCase(), suffix))?.makeVal(); + return { x: comp(":x"), y: comp(":y"), z: comp(":z") }; + } + /** Children of `elt`, plus the refInstance of a Reference so its content is animated by the FX. SubFX and references to FX are animated by their own FXAnimation. @@ -278,17 +342,17 @@ class BaseFXTools { } static var emptyParams : Array = []; - public static function getCustomAnimations(elt: PrefabElement, anims: Array, ?batch: h3d.scene.MeshBatch) { + public static function getCustomAnimations(elt: PrefabElement, anims: Array, ?batch: h3d.scene.MeshBatch, particle = false) { // Init all animations recursively except Emitter ones (called in Emitter) if(Std.downcast(elt, hrt.prefab.fx.Emitter) == null) { for(c in getAnimatedChildren(elt)) { - getCustomAnimations(c, anims, batch); + getCustomAnimations(c, anims, batch, particle); } } var shader = elt.to(hrt.prefab.Shader); if (shader != null && shader.shader != null && !Std.isOfType(shader.shader, hrt.prefab.fx.gpuemitter.ComputeUtils) ) { - var params = makeShaderParams(shader, shader.getShaderDefinition()); + var params = makeShaderParams(shader, shader.getShaderDefinition(), particle); var shader = shader.shader; if(useAutoPerInstance && batch != null) @:privateAccess { diff --git a/hrt/prefab/fx/Curve.hx b/hrt/prefab/fx/Curve.hx new file mode 100644 index 000000000..8ce7c1499 --- /dev/null +++ b/hrt/prefab/fx/Curve.hx @@ -0,0 +1,420 @@ +package hrt.prefab.fx; + +#if editor +import hide.prefab.EditContext; +import hide.prefab.HideProps; +#end +import hrt.prefab.fx.Value; + +using Lambda; + +enum CurveBlendMode { + /** One data, read at the time or at the FX parameter `param` **/ + None; + /** Two datas, blended by the FX parameter `param` **/ + Blend; + /** Two datas, a random between them: the Curve Range of the emitter params **/ + Range; + /** The curve at `refPath` **/ + Reference; +} + +@:prefabIcon(HuiRes.ui.icons.prefab.curve) +class Curve extends hrt.prefab.Curve { + static var CHANNELS = ["position", "rotation", "scale", "color", "visibility", "localposition", "localrotation"]; + + @:isVar public var datas(get, set) : Array; + function get_datas() { + if (datas == null) + datas = [new hrt.impl.Curve()]; + return datas; + } + function set_datas(v) return datas = v; + override function get_data() : hrt.impl.Curve { return datas[0]; } + + override function set_loop(v) { + for (d in datas) + d.loop = v; + return v; + } + + override function get_duration() { + if (blendMode == Reference) + return getRef()?.duration ?? 0.0; + if (blendMode == Blend) + return Math.min(datas[0].duration, datas[1].duration); + return data.duration; + } + + @:s public var blendMode : CurveBlendMode = None; + @:s public var param : String; + @:s public var refPath : String; + var refCurve : Curve = null; + + override function loadData(o:Dynamic) { + if (o.datas == null) { + super.loadData(o); + return; + } + datas = []; + for (d in (o.datas : Array)) { + var c = new hrt.impl.Curve(); + @:privateAccess c.copyFromDynamic(d); + datas.push(c); + } + loop = datas[0].loop; + } + + override function saveData(o:Dynamic) { + // a reference reads the keys of its curve + if (blendMode == Reference) + return; + if (datas.length == 1) + super.saveData(o); + else + o.datas = [for (d in datas) @:privateAccess d.copyToDynamic(null)]; + } + + #if !editor + override function bakeValues() { + if (blendMode != Reference) + for (d in datas) + d.bakeValues(); + } + #end + + override function getBounds(bounds: h2d.col.Bounds = null) { + if (blendMode == Reference) + return getRef()?.getBounds(bounds) ?? bounds ?? new h2d.col.Bounds(); + return data.getBounds(bounds); + } + + override function getVal(time: Float) : Float { + if (blendMode == Reference) { + throw "getVal shoudln't be called on curves with Reference mode"; + } + return data.getVal(time); + } + + override function getSum(time: Float) : Float { + if (blendMode == Reference) + return getRef()?.getSum(time) ?? 0.0; + return data.getSum(time); + } + + override function sample(numPts: Int) { + if (blendMode == Reference) + return getRef()?.sample(numPts) ?? []; + return data.sample(numPts); + } + + public function isPlain() { + return blendMode == None && param == null; + } + + public function isParticleCurve() { + var rootCurve : Prefab = this; + while (rootCurve.parent is Curve) + rootCurve = rootCurve.parent; + var parent = rootCurve.parent; + var emitter : EmitterParam.IEmitter = parent is EmitterParam.IEmitter ? cast parent : null; + if (emitter == null) + return rootCurve.findParent(Prefab, p -> p is EmitterParam.IEmitter) != null; + return !isChannel(rootCurve.name) && !isParamName(emitter, rootCurve.name); + } + + override function makeVal() : Value { + inline function fail(what: String) : Value throw '${getAbsPath(true)}: $what is not supported'; + return switch(blendMode) { + case None: + param == null ? VCurve(data) : VParamCurve({ c1: data, c2: null, param: param }); + case Blend: + VParamCurve({ c1: datas[0], c2: datas[1], param: param }); + case Range: + fail("a random range out of the Curve Range of the emitter params"); + case Reference: + var ref = getRef(); + switch(ref?.makeVal()) { + case null: VConst(0.0); + case v = VConst(_) | VCurve(_) | VParamCurve(_): v; + case v: fail("a reference to " + v); + } + } + } + + public static function getCurve(parent : Prefab, name: String, onlyEnabled=true) : Curve { + return hrt.prefab.Curve.findCurve(parent, name, onlyEnabled, c -> c.to(Curve)); + } + + public static function getCurves(parent: Prefab, prefix: String) : Array { + return hrt.prefab.Curve.findCurves(parent, prefix, c -> c.to(Curve)); + } + + override function copyData(o:hrt.prefab.Curve) { + #if (editor || editor_hl) + datas = []; + for (d in (cast o : Curve).datas) { + var c = new hrt.impl.Curve(); + @:privateAccess c.copyFromOther(d); + datas.push(c); + } + #else + // Optim: curves are cloned for simplicity but particle Values don't even read the new ones + // and reuse the original curve references directly + datas = (cast o : Curve).datas; + #end + } + + override function makeInstance() { + refCurve = null; + } + + function getRef() : Curve { + if (refCurve != null) + return refCurve; + var root = getRoot(false); + refCurve = root.locatePrefab(refPath)?.to(Curve); + if (refCurve == this) refCurve = null; + return refCurve; + } + + /** Two datas for a blend or a range, one else (the second one starts as a copy of the first) **/ + public function syncDatas() { + var two = blendMode == Blend || blendMode == Range; + if (two && datas.length < 2) { + var d = new hrt.impl.Curve(); + @:privateAccess d.copyFromOther(datas[0]); + datas.push(d); + } else if (!two && datas.length > 1) + datas.resize(1); + } + + override function edit2(ctx: hrt.prefab.EditContext2) { + // not super: replaces the inspector of a plain curve + ctx.build( + + + + +
+
Blend Mode
+ +
+
+
+
Parameter
+ +
+
+
+
Curve Path
+ +
+
+
+
Time Remap
+ +
+
+ + '); + + var blendModeSelect = props.find('#blendMode'); + + var refreshBlend = function() { + var parameter = props.find('#parameter'); + if (blendMode != Blend) { + parameter.hide(); + } + else { + var selecta = parameter.find('[field="param"]'); + parameter.show(); + selecta.empty(); + var root = Std.downcast(getRoot(false), FX); + if (root != null) { + for (p in root.parameters) { + selecta.append(new hide.Element('')); + } + selecta.val(param); + } + } + + var reference = props.find('#reference'); + if (blendMode != Reference) { + reference.hide(); + } + else { + var selecta = reference.find('[field="refPath"]'); + reference.show(); + selecta.empty(); + var root = getRoot(false); + var flat = root.flatten(Curve); + for (p in flat) { + if (p == this) continue; + var path = p.getAbsPath(); + selecta.append(new hide.Element('')); + } + selecta.val(refPath); + } + + var remap = props.find('#remap'); + if (blendMode != None) { + remap.hide(); + } + else { + remap.show(); + var select = remap.find('[field=param]'); + select.empty(); + select.append(new hide.Element('')); + var parameters = Std.downcast(getRoot(false), FX)?.parameters ?? []; + for (p in parameters) { + select.append(new hide.Element('')); + } + select.val(param); + } + } + + refreshBlend(); + + blendModeSelect.val(blendMode.getName()); + blendModeSelect.change((_) -> { + var oldBlend = this.blendMode; + var oldParam = this.param; + var oldRef = this.refPath; + var newBlend = CurveBlendMode.createByName(blendModeSelect.val()); + + function exec(undo : Bool) { + blendMode = undo ? oldBlend : newBlend; + param = undo ? oldParam : null; + refPath = undo ? oldRef : null; + syncDatas(); + refreshBlend(); + ctx.onChange(this, "blendMode"); + ctx.rebuildPrefab(this); + } + + exec(false); + ctx.properties.undo.change(Custom(exec)); + }); + + ctx.properties.add(props, this, function(pname) { + if (pname == "refPath") { + refCurve = null; + if (containsRefCycle()) { + hide.Ide.inst.quickMessage("Curve reference create a cycle"); + refPath = null; + refCurve = null; + } + } + if (pname == "param") { + if (param == "") + param = null; + } + refreshBlend(); + @:privateAccess ctx.scene.editor.refreshTree(SceneTree); + ctx.onChange(this, pname); + }); + + var ce = new hide.comp.Timeline(ctx.properties.undo, ctx.properties.element, false); + ce.saveDisplayKey = this.getAbsPath(true); + ce.curves.push(this); + ce.refresh(); + } + + override function getHideProps() : HideProps { + return { icon : "paint-brush", name : "Curve", allowParent : FX.inFX, allowChildren : _ -> false }; + } + #end + + function containsRefCycle(?visited: Map) : Bool { + if (blendMode != Reference) return false; + var visited = visited?.copy() ?? []; + var cur = this; + while (cur != null) { + if (visited.get(cur) != null) + { + return true; + } + visited.set(cur, true); + cur = cur.getRef(); + } + return false; + } + + static function isChannel(name: String) { + return name != null && CHANNELS.contains(name.split(":")[0].toLowerCase()); + } + + static function isParamName(emitter: EmitterParam.IEmitter, name: String) { + var base = name?.split(":")[0]; + return Lambda.exists(emitter.getFields(), f -> switch (f) { + case Property(p): p.animate && p.name == base; + case Param(p): p.kind != Particle && p.name == base; + }); + } + + static var _ = Prefab.register("fxcurve", Curve); +} diff --git a/hrt/prefab/fx/Emitter.hx b/hrt/prefab/fx/Emitter.hx index 4953980f6..2bfdee21e 100644 --- a/hrt/prefab/fx/Emitter.hx +++ b/hrt/prefab/fx/Emitter.hx @@ -1,10 +1,9 @@ package hrt.prefab.fx; import hrt.impl.Gradient; import hrt.prefab.l3d.Polygon; -import hrt.prefab.Curve; import hrt.prefab.fx.Value; import hrt.prefab.fx.Evaluator; -import hrt.prefab.fx.EmitterMacros.param; +import hrt.prefab.fx.EmitterParam; #if editor import hide.prefab.HideProps; @@ -51,6 +50,8 @@ enum EmitShape { Box; Cylinder; Dome; + Point; + Line; } enum Orientation { @@ -62,7 +63,6 @@ enum Orientation { enum EmitType { Infinity; - InfinityRandom; Duration; Burst; BurstDuration; @@ -73,24 +73,10 @@ enum SubEmitterKind { Follow; } -@:publicFields -class InstanceDef { - var localSpeed: Value; - var worldSpeed: Value; - var orbitSpeed: Value; - var orbitSpeedOverTime: Value; - var acceleration: Value; - var worldAcceleration: Value; - var localOffset: Value; - var pivot: Value; - var scale: Value; - var scaleOverTime: Value; - var stretch: Value; - var stretchVelocity: Value; - var rotation: Value; - var dampen: Value; - var maxVelocity : Value; - public function new() { } +enum RandomColorMode { + None; + Colors; + Gradient; } typedef PartArray = #if (hl_ver >= version("1.14.0")) hl.CArray #else Array #end; @@ -125,7 +111,6 @@ class ParticleInstance { var emitOrientation(default, never) = new SMatrix3(); #end - var colorMult : Int; var idx : Int; var startFrame : hxd.impl.UInt16; var life : Single; @@ -151,7 +136,6 @@ class ParticleInstance { absPos.load(p.absPos.toMatrix()); emitOrientation.load(p.emitOrientation.toMatrix()); - colorMult = p.colorMult; idx = p.idx; startFrame = p.startFrame; life = p.life; @@ -194,7 +178,6 @@ class ParticleInstance { scaleY = 1.0; scaleZ = 1.0; - colorMult = -1; if(this.idx != REMOVED_IDX) emitter.error(); this.idx = idx; speedAccumulation.load(new h3d.Vector()); @@ -244,13 +227,14 @@ class ParticleInstance { } function updateAbsPos(emitter: EmitterObject) { + var prefab = emitter.prefab; var v = new h3d.Vector4(); var absPos = tmpMat; var localMat = tmpMat2; - switch( emitter.alignMode ) { + switch( prefab.alignMode ) { case Screen|Axis: - if ( emitter.followRotation ) { + if ( prefab.followRotation ) { inline qRot.toQuat().toMatrix(absPos); absPos.multiply(emitter.screenRot, absPos); } else @@ -294,24 +278,21 @@ class ParticleInstance { var evaluator = emitter.evaluator; //SCALE - var def = emitter.instDef; - evaluator.getFastVec(idx, def.stretch, t, v); - v.scale3(evaluator.getFast(idx, def.scale, t)); - v.scale3(evaluator.getFast(idx, def.scaleOverTime, emitter.curTime)); + evaluator.getPartStretch(idx, t, v); + v.scale3(evaluator.getPartScale(idx, t)); + v.scale3(emitter.curScaleMultiplier); localMat.initScale(v.x, v.y, v.z); // PIVOT - if(def.pivot != VZero) { - evaluator.getFastVec(idx, def.pivot, t, v); + if(evaluator.getPartPivot(idx, t, v)) { localMat.tx = v.x * localMat._11; localMat.ty = v.y * localMat._22; localMat.tz = v.z * localMat._33; } // ROTATION - if(def.rotation != VZero) { - evaluator.getFastVec(idx, def.rotation, t, v); + if(evaluator.getPartRotation(idx, t, v)) { v.scale3(Math.PI / 180.0); if(v.x == 0 && v.y == 0) // optim: most rotations are around Z only, >2x faster EmitterHelper.rotateZ(localMat, v.z); @@ -320,8 +301,7 @@ class ParticleInstance { } //OFFSET - if(def.localOffset != VZero) { - evaluator.getFastVec(idx, def.localOffset, t, v); + if(evaluator.getPartOffset(idx, t, v)) { localMat.tx += v.x; localMat.ty += v.y; localMat.tz += v.z; @@ -341,35 +321,31 @@ class ParticleInstance { var speed = new h3d.Vector(); speed.set(0,0,0); - var def = emitter.instDef; + var prefab = emitter.prefab; var evaluator = emitter.evaluator; var emitOrientation = emitOrientation.toMatrix(); var speedAccumulation = this.speedAccumulation.toVector(); if( life == 0 ) { - // START LOCAL SPEED - evaluator.getFastVec(idx, emitter.startSpeed, emitter.curTime, v); + // local speed + evaluator.getStartSpeed(idx, emitter.curTime, v); v.transform3x3(emitOrientation); add(speedAccumulation, v); - // START WORLD SPEED - evaluator.getFastVec(idx, emitter.startWorldSpeed, emitter.curTime, v); + // world speed + evaluator.getStartWorldSpeed(idx, emitter.curTime, v); v.transform3x3(emitter.invTransform); add(speedAccumulation, v); } - // ACCELERATION - if(def.acceleration != VZero) { - evaluator.getFastVec(idx, def.acceleration, t, v); + if(evaluator.getPartAcceleration(idx, t, v)) { v.scale3(dt); v.transform3x3(emitOrientation); add(speedAccumulation, v); } - // WORLD ACCELERATION - if(def.worldAcceleration != VZero) { - evaluator.getFastVec(idx, def.worldAcceleration, t, v); + if(evaluator.getPartWorldAcceleration(idx, t, v)) { v.scale3(dt); - if(emitter.simulationSpace == Local) + if(prefab.simulationSpace == Local) v.transform3x3(emitter.invTransform); add(speedAccumulation, v); } @@ -378,32 +354,25 @@ class ParticleInstance { speed.y += speedAccumulation.y; speed.z += speedAccumulation.z; - // SPEED - if(def.localSpeed != VZero) { - evaluator.getFastVec(idx, def.localSpeed, t, v); + if(evaluator.getPartSpeed(idx, t, v)) { v.transform3x3(emitOrientation); add(speed, v); } - // DAMPEN - if (def.dampen != VZero) { - var dampen = evaluator.getFast(idx, def.dampen, t); + if (evaluator.partDampen != VZero) { + var dampen = evaluator.getPartDampen(idx, t); var scale = Math.exp(dampen* -dt); speedAccumulation.scale(scale); } - - // WORLD SPEED - if(def.worldSpeed != VZero) { - evaluator.getFastVec(idx, def.worldSpeed, t, v); - if(emitter.simulationSpace == Local) + if(evaluator.getPartWorldSpeed(idx, t, v)) { + if(prefab.simulationSpace == Local) v.transform3x3(emitter.invTransform); add(speed, v); } - // MAX VELOCITY - if (def.maxVelocity != VZero) { - var maxVel = evaluator.getFast(idx, def.maxVelocity, t); + if (evaluator.partMaxSpeed != VZero) { + var maxVel = evaluator.getPartMaxSpeed(idx, t); var curVelSq = speed.lengthSq(); if (maxVel * maxVel < curVelSq) { speed.normalize(); @@ -411,15 +380,14 @@ class ParticleInstance { } } - if(emitter.simulationSpace == World) { + if(prefab.simulationSpace == World) { speed.x *= emitter.worldScale.x; speed.y *= emitter.worldScale.y; speed.z *= emitter.worldScale.z; } - // STRETCH VELOCITY - if (def.stretchVelocity != VZero) { - var s = evaluator.getFast(idx, def.stretchVelocity, t); + if (evaluator.partStretchVelocity != VZero) { + var s = evaluator.getPartStretchVelocity(idx, t); var absPos = this.absPos; var sx = hxd.Math.abs(speed.x * absPos._11 + speed.y * absPos._12 + speed.z * absPos._13); sx = hxd.Math.min(sx, 0.25); @@ -439,11 +407,10 @@ class ParticleInstance { y += speed.y * dt; z += speed.z * dt; - if(def.orbitSpeed != VZero) { - evaluator.getFastVec(idx, def.orbitSpeed, t, v); + if(evaluator.getPartOrbitSpeed(idx, t, v)) { - var factorOverTime = evaluator.getFast(idx, def.orbitSpeedOverTime, emitter.curTime); - v.scale3(factorOverTime); + var factorOverTime = emitter.curOrbitSpeedMultiplier; + v.scale3(factorOverTime * (Math.PI / 180.0)); tmpMat.initRotation(v.x * dt, v.y * dt, v.z * dt); @@ -465,17 +432,16 @@ class ParticleInstance { speed.z += delta.z; } - // COLLISION - if(emitter.useCollision) { + if(prefab.useCollision) { var plane = emitter.collisionPlane; var dist = plane.distance(getPosition()); if(dist < 0) { - var k = emitter.killOnCollision; + var k = prefab.killOnCollision; if(k > 0 && (k >= 1 || emitter.random.rand() < k)) life = lifeTime; var n = plane.getNormal(); var vn = speedAccumulation.dot(n); - var e = vn < 0 ? 1 + emitter.elasticity : 1; + var e = vn < 0 ? 1 + prefab.elasticity : 1; if(vn < 0) { speedAccumulation.x -= n.x * vn * e; speedAccumulation.y -= n.y * vn * e; @@ -490,7 +456,7 @@ class ParticleInstance { this.speedAccumulation.load(speedAccumulation); - if((emitter.emitOrientation == Speed || emitter.alignMode == Speed || emitter.alignMode == ScreenSpeed || emitter.alignMode == AxisSpeed) && speed.lengthSq() > 0.01) { + if((prefab.emitOrientation == Speed || prefab.alignMode == Speed || prefab.alignMode == ScreenSpeed || prefab.alignMode == AxisSpeed) && speed.lengthSq() > 0.01) { var q = qRot.toQuat(); inline q.initDirection(speed); this.qRot.loadQuat(q); @@ -514,18 +480,25 @@ class ParticleInstance { } } +@:build(hrt.prefab.fx.EmitterMacros.buildEvaluator("hrt.prefab.fx.Emitter")) +class EmitterEvaluator extends Evaluator {} + @:access(hrt.prefab.Prefab) @:allow(hrt.prefab.fx.ParticleInstance) @:allow(hrt.prefab.fx.Emitter) class EmitterObject extends h3d.scene.Object { - public var instDef : InstanceDef; + public var prefab : Emitter; + + // Game overrides + public var emitScale : Float = 1.0; + public var lifeTimeScale : Float = 1.0; public var particles : PartArray; public var particlesCount : Int; public var listHead : ParticleInstance; - // the particles added at the head of the list since the last depth sort - var newInstances = 0; + var newInstances = 0; // particles added at the head of the list since the last depth sort + public var batch : h3d.scene.MeshBatch; public var customAnims : Array; @@ -537,7 +510,6 @@ class EmitterObject extends h3d.scene.Object { public var scene : h3d.scene.Scene; public var catchupSpeed = 4; // Use larger ticks when catching-up to save calculations - public var totalBurstCount : Int = 0; // Keep track of burst count #if !editor public var maxCatchupWindow = 0.5; // How many seconds max to simulate when catching up #end @@ -557,81 +529,17 @@ class EmitterObject extends h3d.scene.Object { } #end - public var emitterPrefab : Emitter; public var parentNonEmitter : h3d.scene.Object; - // RANDOM - public var seedGroup = 0; - // OBJECTS + // made from the children of the prefab public var particleTemplate : hrt.prefab.Object3D; public var subEmitterTemplates : Array; public var subEmitters : Array; - public var subEmitterKind : SubEmitterKind; public var trails : hrt.prefab.l3d.Trails.TrailObj; public var trailsTemplate : hrt.prefab.l3d.Trails; - // LIFE - public var lifeTime = 2.0; - public var lifeTimeRand = 0.0; - public var speedFactor = 1.0; - public var warmUpTime = 0.0; - public var delay = 0.0; - // EMIT PARAMS - public var emitOrientation : Orientation = Forward; - public var simulationSpace : SimulationSpace = Local; - public var particleScaling : ParticleScaling = Parent; - public var emitType : EmitType = Infinity; - public var burstCount : Int = 1; - public var burstParticleCount : Value; - public var burstDelay : Float = 1.0; - public var emitDuration : Float = 1.0; - public var emitRate : Value; - public var emitRateMin : Value; - public var emitRateMax : Value; - public var emitRateChangeDelay : Float = 1.0; - public var emitRateChangeDelayStart : Float = 0.0; - public var emitRateCurrent : Float = Math.NaN; - public var emitRatePrevious : Float = Math.NaN; - public var emitRateTarget : Float = Math.NaN; - public var emitScale : Float = 1.0; - public var maxCount = 20; - public var sortMode : SortMode = None; - public var followRotation = false; - public var applyMeshTransform = false; - public var startSpeed: Value; - public var startWorldSpeed: Value; - - // EMIT SHAPE - public var emitShape : EmitShape = Cylinder; - public var emitAngle : Value; - public var emitRad1 : Float = 1.0; - public var emitRad2 : Float = 1.0; - public var emitSurface : Bool = false; - public var emitDomeCut : Float = 0.5; - public var emitDomeFlip : Bool = false; - // ANIMATION - public var spriteSheet : String; - public var frameCount : Int = 0; - public var frameDivisionX : Int = 1; - public var frameDivisionY : Int = 1; - public var animationSpeed : Float = 1; - public var animationLoop : Bool = true; - public var animationUseSourceUVs : Bool = true; - public var animationBlendBetweenFrames : Bool = true; - // ALIGNMENT - public var alignMode : AlignMode; - public var alignLockAxis : AlignLockAxis; - // COLLISION - public var elasticity : Float = 1.0; - public var killOnCollision : Float = 0.0; - public var useCollision : Bool = false; + // Ground Collision var collisionPlane : h3d.col.Plane; - // RANDOM COLOR - public var useRandomColor : Bool = false; - public var useRandomGradient : Bool = false; - public var randomColor1 : h3d.Vector; - public var randomColor2 : h3d.Vector; - public var randomGradient : GradientData; public var invTransform = new h3d.Matrix(); public var screenRot = new h3d.Matrix(); @@ -646,21 +554,24 @@ class EmitterObject extends h3d.scene.Object { var subEmitterSeed = 0; var emitCount = 0; var emitTarget = 0.0; + var totalBurstCount = 0; var curTime = 0.0; - var evaluator : Evaluator; + var curScaleMultiplier = 1.0; + var curOrbitSpeedMultiplier = 1.0; + + public var evaluator : EmitterEvaluator; + var numInstances = 0; var instanceCounter = 0; var baseEmitterShader : hrt.shader.BaseEmitter = null; var animatedTextureShader : h3d.shader.AnimatedTexture = null; - var colorMultShader : h3d.shader.ColorMult = null; + var particleShader : hrt.shader.ParticleShader = null; var parentTransform = new h3d.Matrix(); var orbitToEmitter = new h3d.Matrix(); var orbitFromEmitter = new h3d.Matrix(); var baseEmitMat : h3d.Matrix; var meshTransform = new h3d.Matrix(); - var randomValues : Array; - var randSlots : Int; public function new(?parent) { super(parent); @@ -700,9 +611,12 @@ class EmitterObject extends h3d.scene.Object { scene = getScene(); } - function init(randSlots: Int, prefab: Emitter) { - this.emitterPrefab = prefab; - this.randSlots = randSlots; + function init(prefab: Emitter) { + this.prefab = prefab; + evaluator.allocRandom(prefab.maxCount, evaluator.randomSlots); + var anim = getParentFX(); + if(anim != null) + evaluator.parameters = @:privateAccess anim.evaluator.parameters; updateParentNonEmitter(); @@ -740,7 +654,7 @@ class EmitterObject extends h3d.scene.Object { var absPos = mesh.getAbsPos(); tmpMat.initInverse(baseEmitMat); meshTransform.multiply(absPos, tmpMat); - if (applyMeshTransform) + if (prefab.applyMeshTransform) baseEmitMat.load(absPos); mesh.remove(); } @@ -785,27 +699,28 @@ class EmitterObject extends h3d.scene.Object { } // Setup mats - var materials = findRec(emitterPrefab, hrt.prefab.Material); + var materials = findRec(prefab, hrt.prefab.Material); for( mat in materials ) @:privateAccess mat.makeInstance(); // Setup shaders customAnims = []; - var shaders = findRec(emitterPrefab, hrt.prefab.Shader); + var shaders = findRec(prefab, hrt.prefab.Shader); for( shader in shaders ) { makeShaderInstance(shader); - hrt.prefab.fx.BaseFX.BaseFXTools.getCustomAnimations(shader, customAnims, batch); + hrt.prefab.fx.BaseFX.BaseFXTools.getCustomAnimations(shader, customAnims, batch, true); } // Animated textures animations - var frameCount = frameCount == 0 ? frameDivisionX * frameDivisionY : frameCount; - if( frameCount > 1 && spriteSheet != null ) { - var tex = hxd.res.Loader.currentInstance.load(spriteSheet).toTexture(); - animatedTextureShader = new h3d.shader.AnimatedTexture(tex, frameDivisionX, frameDivisionY, frameCount, frameCount * animationSpeed / lifeTime); - animatedTextureShader.useSourceUVs = animationUseSourceUVs; + var frameCount = prefab.frameCount == 0 ? prefab.frameDivisionX * prefab.frameDivisionY : prefab.frameCount; + if( frameCount > 1 && prefab.spriteSheet != null ) { + var tex = hxd.res.Loader.currentInstance.load(prefab.spriteSheet).toTexture(); + var meanLifeTime = evaluator.getMean(evaluator.lifeTime, 0); + animatedTextureShader = new h3d.shader.AnimatedTexture(tex, prefab.frameDivisionX, prefab.frameDivisionY, frameCount, frameCount * prefab.animationCycles / meanLifeTime); + animatedTextureShader.useSourceUVs = prefab.animationUseSourceUVs; animatedTextureShader.startTime = @:privateAccess scene?.renderer.ctx.globalTime ?? 0.0; - animatedTextureShader.loop = animationLoop; - animatedTextureShader.blendBetweenFrames = animationBlendBetweenFrames; + animatedTextureShader.loop = prefab.animationLoop; + animatedTextureShader.blendBetweenFrames = prefab.animationBlendBetweenFrames; animatedTextureShader.setPriority(1); batch.material.mainPass.addShader(animatedTextureShader); } @@ -815,9 +730,35 @@ class EmitterObject extends h3d.scene.Object { baseEmitterShader.meshTransformInverse.initInverse(meshTransform); batch.material.mainPass.addShader(baseEmitterShader); - if(useRandomColor) { - colorMultShader = new h3d.shader.ColorMult(); - batch.material.mainPass.addShader(colorMultShader); + particleShader = null; + inline function getShader() { + if( particleShader == null ) { + particleShader = new hrt.shader.ParticleShader(); + batch.material.mainPass.addShader(particleShader); + } + return particleShader; + } + if( prefab.randomColorMode != RandomColorMode.None ) { + var s = getShader(); + s.randomMode = prefab.randomColorMode; + s.randomColor1.setColor(prefab.randomColor1); + s.randomColor2.setColor(prefab.randomColor2); + if( prefab.randomColorMode == RandomColorMode.Gradient ) + s.randomGradient = hrt.impl.Gradient.textureFromData(prefab.randomGradient, false); + } + if( prefab.fadeColorMode != RandomColorMode.None ) { + var s = getShader(); + s.fadeMode = prefab.fadeColorMode; + s.fadeColor1.setColor(prefab.fadeColor1); + s.fadeColor2.setColor(prefab.fadeColor2); + if( prefab.fadeColorMode == RandomColorMode.Gradient ) + s.fadeGradient = hrt.impl.Gradient.textureFromData(prefab.fadeGradient, false); + } + if( prefab.fadeIn > 0 || prefab.fadeOut > 0 ) { + var s = getShader(); + s.alphaFade = true; + s.fadeIn = prefab.fadeIn; + s.fadeOut = prefab.fadeOut; } } @@ -829,20 +770,13 @@ class EmitterObject extends h3d.scene.Object { } } - if(trails != null && trails.numTrails != maxCount) { + if(trails != null && trails.numTrails != prefab.maxCount) { trails.remove(); trails = null; } - particles = #if (hl_ver >= version("1.14.0")) hl.CArray.alloc(ParticleInstance, maxCount) #else [for(i in 0...maxCount) new ParticleInstance()] #end; - particlesCount = maxCount; - randomValues = [for(i in 0...(maxCount * randSlots)) 0]; - evaluator = new Evaluator(randomValues, randSlots); - - var anim = getParentFX(); - if(anim != null) - evaluator.parameters = @:privateAccess anim.evaluator.parameters; - + particles = #if (hl_ver >= version("1.14.0")) hl.CArray.alloc(ParticleInstance, prefab.maxCount) #else [for(i in 0...prefab.maxCount) new ParticleInstance()] #end; + particlesCount = prefab.maxCount; reset(); } @@ -887,15 +821,12 @@ class EmitterObject extends h3d.scene.Object { random.init(randomSeed); numInstances = 0; instanceCounter = 0; - curTime = -delay; + curTime = -prefab.delay; emitCount = 0; emitTarget = 0; listHead = null; - if(randomValues != null) { - for(i in 0...randomValues.length) - randomValues[i] = random.srand(); - } + evaluator?.resetRandom(random); if(particles != null) { for(i in 0...particlesCount) { @@ -968,7 +899,7 @@ class EmitterObject extends h3d.scene.Object { #end function allocInstance() { - if(numInstances >= maxCount) error("allocInstance"); + if(numInstances >= prefab.maxCount) error("allocInstance"); var p = particles[numInstances++]; p.init(instanceCounter, this); p.prev = null; @@ -977,7 +908,7 @@ class EmitterObject extends h3d.scene.Object { listHead.prev = p; listHead = p; newInstances++; - instanceCounter = (instanceCounter + 1) % maxCount; + instanceCounter = (instanceCounter + 1) % prefab.maxCount; checkList(); return p; } @@ -1047,7 +978,8 @@ class EmitterObject extends h3d.scene.Object { if( count == 0 ) return; - if( instDef == null) + // the values are not made yet + if( evaluator == null ) return; @:privateAccess var globalTime = scene?.renderer.ctx.globalTime ?? curTime; @@ -1056,7 +988,7 @@ class EmitterObject extends h3d.scene.Object { if (count > 0) { if (trailsTemplate != null && trails == null) { - trails = trailsTemplate.create(this, maxCount); + trails = trailsTemplate.create(this, prefab.maxCount); trailsTemplate.local3d = trails; trails.autoTrackPosition = false; for( mat in trailsTemplate.findAll(hrt.prefab.Material, checkEnabled) ) @@ -1068,45 +1000,34 @@ class EmitterObject extends h3d.scene.Object { for( i in 0...count ) { var part = allocInstance(); part.startTime = globalTime; - part.lifeTime = hxd.Math.max(0.01, lifeTime + random.srand(lifeTimeRand)); - - if(useRandomColor) { - if (useRandomGradient) { - part.colorMult = Gradient.evalData(randomGradient, random.rand()).toColor(); - } - else { - var col = new h3d.Vector(); - col.lerp(randomColor1, randomColor2, random.rand()); - part.colorMult = col.toColor(); - } - } + part.lifeTime = hxd.Math.max(0.01, evaluator.getLifeTime(part.idx, curTime) * lifeTimeScale); tmpQuat.identity(); - switch( emitShape ) { + switch( prefab.emitShape ) { case Box: tmpOffset.set(random.srand(0.5), random.srand(0.5), random.srand(0.5)); - if( emitSurface ) { + if( prefab.emitSurface ) { var max = Math.max(Math.max(Math.abs(tmpOffset.x), Math.abs(tmpOffset.y)), Math.abs(tmpOffset.z)); tmpOffset.scale(0.5 / max); } - if( emitOrientation == Normal ) + if( prefab.emitOrientation == Normal ) tmpQuat.initDirection(tmpOffset); case Cylinder: var z = random.rand(); var dx = 0.0, dy = 0.0; - var shapeAngle = hxd.Math.degToRad(evaluator.getFloat(emitAngle, curTime)) / 2.0; + var shapeAngle = hxd.Math.degToRad(evaluator.getEmitAngle(curTime)) / 2.0; var a = random.srand(shapeAngle); - if(emitSurface) { - dx = Math.cos(a)*(emitRad2*z + emitRad1*(1.0-z)); - dy = Math.sin(a)*(emitRad2*z + emitRad1*(1.0-z)); + if(prefab.emitSurface) { + dx = Math.cos(a)*(prefab.emitRad2*z + prefab.emitRad1*(1.0-z)); + dy = Math.sin(a)*(prefab.emitRad2*z + prefab.emitRad1*(1.0-z)); } else { - dx = Math.cos(a)*(emitRad2*z + emitRad1*(1.0-z))*random.rand(); - dy = Math.sin(a)*(emitRad2*z + emitRad1*(1.0-z))*random.rand(); + dx = Math.cos(a)*(prefab.emitRad2*z + prefab.emitRad1*(1.0-z))*random.rand(); + dy = Math.sin(a)*(prefab.emitRad2*z + prefab.emitRad1*(1.0-z))*random.rand(); } tmpOffset.set(dx * 0.5, dy * 0.5, z - 0.5); - if( emitOrientation == Normal ) + if( prefab.emitOrientation == Normal ) tmpQuat.initRotation(0, 0, hxd.Math.atan2(dy, dx)); case Sphere: do { @@ -1115,14 +1036,14 @@ class EmitterObject extends h3d.scene.Object { tmpOffset.z = random.srand(1.0); } while( tmpOffset.lengthSq() > 1.0 ); - if( emitSurface ) + if( prefab.emitSurface ) tmpOffset.normalize(); tmpOffset.scale(0.5); - if( emitOrientation == Normal ) + if( prefab.emitOrientation == Normal ) tmpQuat.initDirection(tmpOffset); case Dome: - var zMin = 1.0 - 2.0 * hxd.Math.clamp(emitDomeCut, 0, 1); - if( emitSurface ) { + var zMin = 1.0 - 2.0 * hxd.Math.clamp(prefab.emitDomeHeight, 0, 1); + if( prefab.emitSurface ) { var z = zMin + (1.0 - zMin) * random.rand(); var r = Math.sqrt(1.0 - z * z); var a = random.rand() * Math.PI * 2; @@ -1137,15 +1058,33 @@ class EmitterObject extends h3d.scene.Object { } while( tmpOffset.lengthSq() > 1.0 ); } - if( emitDomeFlip ) + if( prefab.emitDomeFlip ) tmpOffset.z = -tmpOffset.z; tmpOffset.scale(0.5); - if( emitOrientation == Normal ) + if( prefab.emitOrientation == Normal ) tmpQuat.initDirection(tmpOffset); + case Point: + tmpOffset.set(0, 0, 0); + if( prefab.emitOrientation == Normal ) { + do { + tmpDir.x = random.srand(1.0); + tmpDir.y = random.srand(1.0); + tmpDir.z = random.srand(1.0); + } + while( tmpDir.lengthSq() > 1.0 || tmpDir.lengthSq() < 1e-6 ); + tmpQuat.initDirection(tmpDir); + } + case Line: + tmpOffset.set(random.srand(0.5), 0, 0); + if( prefab.emitOrientation == Normal ) { + var a = random.rand() * Math.PI * 2; + tmpDir.set(0, Math.cos(a), Math.sin(a)); + tmpQuat.initDirection(tmpDir); + } case Cone: tmpOffset.set(0, 0, 0); var theta = random.rand() * Math.PI * 2; - var shapeAngle = hxd.Math.degToRad(evaluator.getFloat(emitAngle, curTime)) / 2.0; + var shapeAngle = hxd.Math.degToRad(evaluator.getEmitAngle(curTime)) / 2.0; var phi = shapeAngle * random.rand(); tmpDir.x = Math.cos(phi) * scaleX; tmpDir.y = Math.sin(phi) * Math.sin(theta) * scaleY; @@ -1156,10 +1095,10 @@ class EmitterObject extends h3d.scene.Object { onEmit(tmpOffset, tmpQuat); - if( emitOrientation == Random ) + if( prefab.emitOrientation == Random ) tmpQuat.initRotation(random.srand(Math.PI), random.srand(Math.PI), random.srand(Math.PI)); - switch( simulationSpace ) { + switch( prefab.simulationSpace ) { case Local: if(emitterQuat == null) { emitterQuat = tmpEmitterQuat; @@ -1168,7 +1107,7 @@ class EmitterObject extends h3d.scene.Object { tmpMat2.load(parentNonEmitter.getAbsPos()); tmpMat2.invert(); tmpMat.multiply(tmpMat, tmpMat2); - if (particleScaling == None) { + if (prefab.particleScaling == None) { // // Re-introduce parent scaling in the spawn position calculations var parentScale = inline parentNonEmitter.getAbsPos().getScale(); tmpMat2.initScale(parentScale.x, parentScale.y, parentScale.z); @@ -1202,8 +1141,8 @@ class EmitterObject extends h3d.scene.Object { part.setScale(worldScale.x, worldScale.y, worldScale.z); } - var frameCount = frameCount == 0 ? frameDivisionX * frameDivisionY : frameCount; - if(animationLoop) + var frameCount = prefab.frameCount == 0 ? prefab.frameDivisionX * prefab.frameDivisionY : prefab.frameCount; + if(prefab.animationLoop) part.startFrame = random.random(frameCount); if (subEmitterTemplates != null) { @@ -1269,6 +1208,14 @@ class EmitterObject extends h3d.scene.Object { } } + inline function getBurstCount() { + return EmitterHelper.getBurstCount(evaluator, evaluator.burstParticleCount, curTime, totalBurstCount, randomSeed); + } + + inline function getRate() { + return EmitterHelper.getRate(evaluator, evaluator.emitRate, curTime, prefab.emitRateChangeTime, randomSeed); + } + function tick( dt : Float, full=true) { if (scene == null) scene = getScene(); @@ -1277,13 +1224,15 @@ class EmitterObject extends h3d.scene.Object { debugGraphics.clear(); #end - // Auto remove of sub emitters - if( !enable && (particles == null || numInstances <= 0) && isSubEmitter ) { - parentEmitter.subEmitters.remove(this); + // Auto remove of sub emitters: a Follow one is in the list of its particle until the particle dies + if( !enable && (particles == null || numInstances <= 0) && isSubEmitter && parentEmitter.subEmitters?.remove(this) == true ) { remove(); return; } + curScaleMultiplier = evaluator.getScaleMultiplier(curTime); + curOrbitSpeedMultiplier = evaluator.getOrbitSpeedMultiplier(curTime); + if(subEmitters != null) { for( se in subEmitters ) { se.tick(dt); @@ -1296,11 +1245,11 @@ class EmitterObject extends h3d.scene.Object { } } - if( emitRate == null || emitRate == VZero ) + if( evaluator == null ) return; if( parentNonEmitter != null ) { - if (particleScaling == None) { + if (prefab.particleScaling == None) { worldScale.set(1.0,1.0,1.0); } else { @@ -1310,75 +1259,48 @@ class EmitterObject extends h3d.scene.Object { } if( enable && emitScale > 0. && curTime >= 0) { - switch emitType { + switch prefab.emitType { case Infinity: - emitTarget += evaluator.getFloat(emitRate, curTime) * hxd.Math.clamp(emitScale) * dt; - var delta = hxd.Math.ceil(hxd.Math.min(maxCount - numInstances, emitTarget - emitCount)); - doEmit(delta); - if( isSubEmitter && (parentEmitter == null || parentEmitter.parent == null) ) - enable = false; - case InfinityRandom: - // Every emitRateChangeDelay time, we choose a new emit rate value between min and max - // and lerp from previous emit rate value to the new one, to ensure - // there's no "step" effect visible (we want the emit rate changes to - // be smooth). - - var min = evaluator.getFloat(emitRateMin, curTime); - var max = evaluator.getFloat(emitRateMax, curTime); - - var unInitiliazed = Math.isNaN(emitRateCurrent); - var needNewValue = curTime - emitRateChangeDelayStart >= emitRateChangeDelay || curTime - emitRateChangeDelayStart < 0; - if (unInitiliazed || needNewValue) { - emitRateChangeDelayStart = curTime; - emitRatePrevious = emitRateTarget; - emitRateTarget = random.rand() * (max-min) + min; - - if ( Math.isNaN(emitRatePrevious) ) - emitRatePrevious = random.rand() * (max-min) + min; - } - - var t = (curTime - emitRateChangeDelayStart) / emitRateChangeDelay; - emitRateCurrent = (emitRatePrevious - emitRateTarget) * t + emitRatePrevious; - - emitTarget += emitRateCurrent * hxd.Math.clamp(emitScale) * dt; - - var delta = hxd.Math.ceil(hxd.Math.min(maxCount - numInstances, emitTarget - emitCount)); + emitTarget += getRate() * hxd.Math.clamp(emitScale) * dt; + var delta = hxd.Math.ceil(hxd.Math.min(prefab.maxCount - numInstances, emitTarget - emitCount)); doEmit(delta); if( isSubEmitter && (parentEmitter == null || parentEmitter.parent == null) ) enable = false; case Duration: - emitTarget += evaluator.getFloat(emitRate, hxd.Math.min(curTime, emitDuration)) * hxd.Math.clamp(emitScale) * dt; - var delta = hxd.Math.ceil(hxd.Math.min(maxCount - numInstances, emitTarget - emitCount)); - doEmit(delta); - if( isSubEmitter && curTime >= emitDuration ) + if( curTime < prefab.emitDuration ) { + emitTarget += getRate() * hxd.Math.clamp(emitScale) * dt; + var delta = hxd.Math.ceil(hxd.Math.min(prefab.maxCount - numInstances, emitTarget - emitCount)); + doEmit(delta); + } + if( isSubEmitter && curTime >= prefab.emitDuration ) enable = false; case BurstDuration: - if( burstDelay > 0 ) { - var burstTarget = 1 + hxd.Math.floor(curTime / burstDelay); - var lastBurstTime = totalBurstCount * burstDelay; - var nextBurstTime = lastBurstTime + burstDelay; - var needBurst = nextBurstTime <= emitDuration && totalBurstCount < burstTarget; + if( prefab.burstInterval > 0 ) { + var burstTarget = 1 + hxd.Math.floor(curTime / prefab.burstInterval); + var lastBurstTime = totalBurstCount * prefab.burstInterval; + var nextBurstTime = lastBurstTime + prefab.burstInterval; + var needBurst = nextBurstTime <= prefab.emitDuration && totalBurstCount < burstTarget; while( needBurst ) { - var delta = hxd.Math.ceil(hxd.Math.min(maxCount - numInstances, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); + var delta = hxd.Math.ceil(hxd.Math.min(prefab.maxCount - numInstances, getBurstCount())); doEmit(delta); totalBurstCount++; - lastBurstTime += burstDelay; - nextBurstTime = lastBurstTime + burstDelay; - needBurst = nextBurstTime <= emitDuration && totalBurstCount < burstTarget; + lastBurstTime += prefab.burstInterval; + nextBurstTime = lastBurstTime + prefab.burstInterval; + needBurst = nextBurstTime <= prefab.emitDuration && totalBurstCount < burstTarget; } } - if( isSubEmitter && curTime > emitDuration ) + if( isSubEmitter && curTime > prefab.emitDuration ) enable = false; case Burst: - if( burstDelay > 0 ) { - var burstTarget = hxd.Math.min(burstCount, 1 + hxd.Math.floor(curTime / burstDelay)); + if( prefab.burstInterval > 0 ) { + var burstTarget = hxd.Math.min(prefab.burstCount, 1 + hxd.Math.floor(curTime / prefab.burstInterval)); while( totalBurstCount < burstTarget ) { - var delta = hxd.Math.ceil(hxd.Math.min(maxCount - numInstances, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); + var delta = hxd.Math.ceil(hxd.Math.min(prefab.maxCount - numInstances, getBurstCount())); doEmit(delta); totalBurstCount++; } } - if( isSubEmitter && totalBurstCount == burstCount ) + if( isSubEmitter && totalBurstCount == prefab.burstCount ) enable = false; } @@ -1396,14 +1318,14 @@ class EmitterObject extends h3d.scene.Object { } function updateAlignment() { - if(alignMode == Screen || alignMode == ScreenSpeed || alignMode == AxisSpeed) { + if(prefab.alignMode == Screen || prefab.alignMode == ScreenSpeed || prefab.alignMode == AxisSpeed) { var cam = scene?.camera; if (cam == null) return; tmpMat.load(cam.mcam); tmpMat.invert(); - if(simulationSpace == Local) { // Compensate parent rotation + if(prefab.simulationSpace == Local) { // Compensate parent rotation tmpMat2.load(parentNonEmitter.getAbsPos()); var s = tmpMat2.getScale(); tmpMat2.prependScale(1.0 / s.x, 1.0 / s.y, 1.0 / s.z); @@ -1414,14 +1336,14 @@ class EmitterObject extends h3d.scene.Object { screenRot.initRotationAxis( new h3d.Vector(1,0,0),Math.PI); // Flip Y axis so Y is pointing down screenRot.multiply(screenRot, tmpMat); } - else if(alignMode == Axis) { + else if(prefab.alignMode == Axis) { var cam = scene?.camera; if (cam == null) return; var lockAxis = new h3d.Vector(); var rightAxis = new h3d.Vector(); var upAxis = new h3d.Vector(); - switch alignLockAxis { + switch prefab.alignLockAxis { case X: lockAxis.set(1, 0, 0); rightAxis.set(0, 1, 0); @@ -1442,7 +1364,7 @@ class EmitterObject extends h3d.scene.Object { var lookAtPos = tmpVec; lookAtPos.load(cam.pos); - if ( followRotation ) { + if ( prefab.followRotation ) { var invAbsPos = getInvPos(); lookAtPos.transform(invAbsPos); } else { @@ -1463,7 +1385,7 @@ class EmitterObject extends h3d.scene.Object { angle = -angle; screenRot.initRotationAxis(lockAxis, angle); - if(alignLockAxis == ScreenZ) { + if(prefab.alignLockAxis == ScreenZ) { tmpMat.initRotationAxis(new h3d.Vector(1.0, 0.0, 0.0),-Math.PI/2); screenRot.multiply(tmpMat, screenRot); } @@ -1483,7 +1405,7 @@ class EmitterObject extends h3d.scene.Object { function updateMeshBatch() { if(batch == null) return; - batch.begin(hxd.Math.nextPOT(maxCount)); + batch.begin(hxd.Math.nextPOT(prefab.maxCount)); inline function emit(p: ParticleInstance) { if (p.life > p.lifeTime) @@ -1503,12 +1425,10 @@ class EmitterObject extends h3d.scene.Object { baseEmitterShader.lifeTime = p.lifeTime; baseEmitterShader.random = p.random; - if(colorMultShader != null) - colorMultShader.color.setColor(p.colorMult); batch.emitInstance(); } - switch(sortMode) { + switch(prefab.sortMode) { case Depth: depthSort(); var p = listHead; @@ -1532,7 +1452,7 @@ class EmitterObject extends h3d.scene.Object { if (scene == null) return; - switch(simulationSpace){ + switch(prefab.simulationSpace){ // Particles in Local are spawned next to emitter in the scene tree, // so emitter shape can be transformed (especially scaled) without affecting children case Local : parentTransform.load(parentNonEmitter.getAbsPos()); @@ -1540,12 +1460,12 @@ class EmitterObject extends h3d.scene.Object { // Optim: set to null if identity to skip multiply in particle updates } - if (particleScaling == None) { + if (prefab.particleScaling == None) { var scale = parentTransform.getScale(); parentTransform.scale(1.0/scale.x, 1.0/scale.y, 1.0/scale.z); } - if (useCollision) { + if (prefab.useCollision) { var m = new h3d.Matrix(); m.initInverse(parentTransform); m.multiply(parentNonEmitter.getAbsPos(), m); @@ -1555,7 +1475,7 @@ class EmitterObject extends h3d.scene.Object { } // Particle space <-> emitter space, for orbit speed - if (instDef.orbitSpeed != VZero) { + if (evaluator.partOrbitSpeed != VZero) { orbitToEmitter.multiply(parentTransform, getInvPos()); orbitFromEmitter.initInverse(orbitToEmitter); } @@ -1601,7 +1521,7 @@ class EmitterObject extends h3d.scene.Object { if(full) { p.updateAbsPos(this); - if(p.distToCam < 0 || sortMode == Depth) + if(p.distToCam < 0 || prefab.sortMode == Depth) p.distToCam = camPos.distanceSq(p.absPos.getPosition()); } p.life += dt; // After updateAbsPos(), which uses current life @@ -1634,7 +1554,7 @@ class EmitterObject extends h3d.scene.Object { } public function setRandSeed(seed: Int) { - randomSeed = seed ^ seedGroup; + randomSeed = seed ^ prefab.seed; reset(); } @@ -1645,7 +1565,7 @@ class EmitterObject extends h3d.scene.Object { var lastParentTime = 0.0; public function setTime(parentTime: Float, dt: Float, seek: Bool, full: Bool) { - var localTime = (parentTime-delay) * speedFactor; + var localTime = (parentTime-prefab.delay) * prefab.timeScale; if (!full) { curTime = localTime; return; @@ -1665,13 +1585,14 @@ class EmitterObject extends h3d.scene.Object { else if (!seek) { if (parentTime < lastParentTime) { softReset(); - if (burstDelay > 0) - totalBurstCount = hxd.Math.ceil(hxd.Math.max(0.0, localTime/burstDelay)); - if (emitType == Burst) { - if (totalBurstCount >= burstCount) + // the bursts before this tick are done, the ones since the start of the loop are emitted by it + if (prefab.burstInterval > 0) + totalBurstCount = hxd.Math.ceil(hxd.Math.max(0.0, (localTime - dt * prefab.timeScale) / prefab.burstInterval)); + if (prefab.emitType == Burst) { + if (totalBurstCount >= prefab.burstCount) enable = false; - } else if (emitType == BurstDuration) { - if (curTime > emitDuration) + } else if (prefab.emitType == BurstDuration) { + if (localTime > prefab.emitDuration) enable = false; } @@ -1683,7 +1604,7 @@ class EmitterObject extends h3d.scene.Object { var abs = this.getAbsPos(); baseEmitterShader.emitPosition.set(abs.tx, abs.ty, abs.tz); - var time = localTime + warmUpTime; + var time = localTime + prefab.warmUpTime; var catchupTime = time - curTime; var seek = seek || (catchupTime > 1.5 * dt); @@ -1694,7 +1615,7 @@ class EmitterObject extends h3d.scene.Object { #if editor var start = haxe.Timer.stamp(); #end - tick(dt * speedFactor, true); + tick(dt * prefab.timeScale, true); #if editor var end = haxe.Timer.stamp(); tickTime = end - start; @@ -1715,10 +1636,10 @@ class EmitterObject extends h3d.scene.Object { #if !editor // Limit catchup time to avoid spikes when showing long running FX var longCatchup = catchupTime > maxCatchupWindow; if(longCatchup) { - var firstWarmup = curTime <= 0.0 && warmUpTime > 0; - catchupTime = firstWarmup ? warmUpTime : maxCatchupWindow; + var firstWarmup = curTime <= 0.0 && prefab.warmUpTime > 0; + catchupTime = firstWarmup ? prefab.warmUpTime : maxCatchupWindow; curTime = time - catchupTime; - emitCount = hxd.Math.ceil(evaluator.getSum(emitRate, curTime)); + emitCount = hxd.Math.ceil(EmitterHelper.getRateSum(evaluator, evaluator.emitRate, prefab.emitType == Duration ? Math.min(curTime, prefab.emitDuration) : curTime)); // Force sort after long time invisible for(i in 0...numInstances) @@ -1726,7 +1647,7 @@ class EmitterObject extends h3d.scene.Object { } #end - var catchupTickRate = hxd.Timer.wantedFPS * speedFactor / catchupSpeed; + var catchupTickRate = hxd.Timer.wantedFPS * prefab.timeScale / catchupSpeed; var numTicks = hxd.Math.ceil(catchupTickRate * catchupTime); for(i in 0...numTicks) { @@ -1751,162 +1672,167 @@ class EmitterObject extends h3d.scene.Object { @:prefabName("Emitter") @:prefabIcon(hrt.ui.HuiRes.ui.icons.prefab.emitter) -@:build(hrt.prefab.fx.EmitterMacros.build()) -class Emitter extends Object3D { - - // PROPERTIES - @:param({ t: PFloat(0, 10), group: "Properties" }) - public var lifeTime : Float = 1.0; - @:param({ t: PFloat(0, 1), group: "Properties" }) - public var lifeTimeRand : Float = 0.0; - @:param({ t: PEnum(SubEmitterKind), disp: "Sub Kind", group: "Properties" }) - public var subEmitterKind : SubEmitterKind = SubEmitterKind.SpawnOnDeath; - @:param({ t: PFloat(0, 1), disp: "Speed Factor", group: "Properties" }) - public var speedFactor : Float = 1.0; - @:param({ t: PFloat(0, 1), disp: "Warm Up", group: "Properties" }) - public var warmUpTime : Float = 0.0; - @:param({ t: PFloat(0, 10), disp: "Delay", group: "Properties" }) +@:build(hrt.prefab.fx.EmitterMacros.build("hrt.prefab.fx.Emitter.EmitterEvaluator")) +class Emitter extends Object3D implements hrt.prefab.fx.EmitterParam.IEmitter { + + @:group("Properties") + @:s @:range(0, null) @:disp("Time Scale", "Time multiplier of the simulation") + public var timeScale : Float = 1.0; + @:s @:range(0, null) @:disp("Start Delay", "Time before the start of the emitter, in seconds") public var delay : Float = 0.0; - @:param({ t: PInt(0, 100), disp: "Seed", group: "Properties" }) - public var seedGroup : Int = 0; - @:param({ t: PEnum(AlignMode), disp: "Mode", group: "Properties" }) - public var alignMode : AlignMode = AlignMode.None; - @:param({ t: PEnum(AlignLockAxis), disp: "Lock Axis", group: "Properties" }) - public var alignLockAxis : AlignLockAxis = AlignLockAxis.ScreenZ; - @:param({ t: PEnum(SimulationSpace), disp: "Simulation Space", group: "Properties" }) + @:s @:range(0, null) @:disp("Warm Up", "Time simulated before the start, in seconds: the emitter starts already filled") + public var warmUpTime : Float = 0.0; + @:s @:range(0, null) @:disp("Seed", "Changes the random values of the emitter, to differentiate two identical emitters") + public var seed : Int = 0; + @:s @:disp("Simulation Space", "Local: the particles move with the emitter. World: once emitted, they no longer follow it") public var simulationSpace : SimulationSpace = SimulationSpace.Local; - @:param({ t: PEnum(ParticleScaling), disp: "Scaling", group: "Properties" }) + @:s @:disp("Particle Scaling", "Whether the particles are scaled by the parents of the emitter") public var particleScaling : ParticleScaling = ParticleScaling.Parent; - @:param({ t: PBool, disp: "Follow rotation", group: "Properties" }) - public var followRotation : Bool = false; - @:param({ t: PBool, disp: "Apply mesh transform", group: "Properties" }) + @:s @:disp("Apply Mesh Transform", "The particles take the transform of the mesh of the emitter") public var applyMeshTransform : Bool = false; - @:param({ t: PBool, disp: "Enable Sort", group: "Properties" }) - public var enableSort : Bool = true; - @:param({ t: PEnum(SortMode), disp: "Sort mode", group: "Properties" }) + @:s @:disp("Sort Mode", "Depth: back to front from the camera. Instance: in their emission order. None: not sorted") public var sortMode : SortMode = SortMode.Depth; + @:s @:disp("Sub Emitter Mode", "For a sub emitter: spawned at the death of each particle of its parent, or following each of them") + public var subEmitterKind : SubEmitterKind = SubEmitterKind.SpawnOnDeath; - // EMIT PARAMS - @:param({ t: PEnum(EmitType), disp: "Type", group: "Emit Params" }) + @:group("Emission") + @:s @:disp("Type", "Infinity: emits at its rate. Duration: emits at its rate during a duration. Burst / Burst Duration: groups of particles at regular intervals") public var emitType : EmitType = EmitType.Infinity; - @:param({ t: PFloat(0, 10.0), disp: "Duration", group: "Emit Params" }) + @:s @:range(0, null) @:disp("Duration", "Emission duration, in seconds") public var emitDuration : Float = 1.0; - @:param({ t: PInt(0, 100), disp: "Rate", animate: true, group: "Emit Params" }) - public var emitRate : Float = 5; - @:param({ t: PInt(0, 100), disp: "Rate Min", animate: true, group: "Emit Params" }) - public var emitRateMin : Float = 5; - @:param({ t: PInt(0, 100), disp: "Rate Max", animate: true, group: "Emit Params" }) - public var emitRateMax : Float = 5; - @:param({ t: PFloat(0.01, 5.0), disp: "Rate Change Time", group: "Emit Params" }) - public var emitRateChangeDelay : Float = 1.0; - @:param({ t: PInt(1, 10), disp: "Count", group: "Emit Params" }) + @:param(PFloat(0), Emission, 5.) @:disp("Rate", "Number of particles emitted per second. A Range changes it at random between min and max, every Rate Change Time") + public var emitRate : ParamMode; + @:s @:range(0.01, null) @:disp("Rate Change Time", "Random rate: every this many seconds a new random rate is picked between min and max, and the rate goes to it during this time") + public var emitRateChangeTime : Float = 1.0; + @:s @:range(1, null) @:disp("Burst Count", "Number of bursts") public var burstCount : Float = 1; - @:param({ t: PFloat(0, 1.0), disp: "Delay", group: "Emit Params" }) - public var burstDelay : Float = 1.0; - @:param({ t: PInt(1, 10), disp: "Particle Count", animate: true, group: "Emit Params" }) - public var burstParticleCount : Float = 1; - @:param({ t: PInt(0, 100), group: "Emit Params" }) + @:s @:range(0, null) @:disp("Burst Interval", "Time between two bursts, in seconds") + public var burstInterval : Float = 1.0; + @:param(PFloat(1), Emission, 1.) @:disp("Particles Per Burst", "Number of particles emitted by each burst. A Range draws it for each burst, between min and max") + public var burstParticleCount : ParamMode; + @:s @:range(0, null) @:disp("Max Count", "Maximum number of particles alive at the same time: no particle is emitted beyond") public var maxCount : Int = 20; + @:param(PFloat(0), Spawn, 1.) @:disp("Lifetime", "Life duration of the particle, in seconds") + public var lifeTime : ParamMode; - // EMIT SHAPE - @:param({ t: PEnum(EmitShape), disp: "Shape", group: "Emit Shape" }) + @:group("Emit Shape") + @:s @:disp("Shape", "Volume in which the particles are emitted") public var emitShape : EmitShape = EmitShape.Sphere; - @:param({ t: PFloat(0, 360.0), disp: "Angle", animate: true, group: "Emit Shape" }) + @:s @:range(0, 360.0) @:animate @:disp("Angle", "Opening angle of the cone, or angle of the cylinder sector, in degrees") public var emitAngle : Float = 30.0; - @:param({ t: PFloat(0, 1.0), disp: "Radius 1", group: "Emit Shape" }) + @:s @:range(0, null) @:disp("Bottom Radius", "Radius of the bottom of the cylinder") public var emitRad1 : Float = 1.0; - @:param({ t: PFloat(0, 1.0), disp: "Radius 2", group: "Emit Shape" }) + @:s @:range(0, null) @:disp("Top Radius", "Radius of the top of the cylinder") public var emitRad2 : Float = 1.0; - @:param({ t: PBool, disp: "Surface", group: "Emit Shape" }) - public var emitSurface : Bool = false; - @:param({ t: PFloat(0, 1.0), disp: "Cut", group: "Emit Shape" }) - public var emitDomeCut : Float = 0.5; - @:param({ t: PBool, disp: "Flip", group: "Emit Shape" }) + @:s @:range(0, null) @:disp("Dome Height", "Height of the dome: 0.5 for a half sphere, 1 for a full sphere") + public var emitDomeHeight : Float = 0.5; + @:s @:disp("Flip Dome", "Flips the dome upside down") public var emitDomeFlip : Bool = false; - @:param({ t: PEnum(Orientation), disp: "Orientation", group: "Emit Params" }) + @:s @:disp("Surface", "Emits on the surface of the shape instead of in its volume") + public var emitSurface : Bool = false; + @:s @:disp("Orientation", "Initial orientation of the particles: the emitter one, the normal of the shape, their speed, or random") public var emitOrientation : Orientation = Orientation.Forward; - // COLOR - @:param({ t: PBool, disp: "Random Color", group: "Color" }) - public var useRandomColor : Bool = false; - @:param({ t: PBool, disp: "Random Gradient", group: "Color" }) - public var useRandomGradient : Bool = false; - @:param({ t: PVec(4), disp: "Color 1", group: "Color" }) - public var randomColor1 : Array = [0., 0., 0., 1.]; - @:param({ t: PVec(4), disp: "Color 2", group: "Color" }) - public var randomColor2 : Array = [1., 1., 1., 1.]; - @:param({ t: PGradient, disp: "Gradient", group: "Color" }) - public var randomGradient : GradientData; + @:group("Particle Movement") + @:param(PVec(3), Spawn) @:disp("Start Speed", "Initial speed in the emitter orientation") + public var startSpeed : ParamVec3; + @:param(PVec(3), Spawn) @:disp("Start World Speed", "Initial speed in world space") + public var startWorldSpeed : ParamVec3; + @:param(PVec(3), Particle) @:disp("Speed", "Speed in the emitter orientation, added to the velocity without being accumulated") + public var partSpeed : ParamVec3; + @:param(PVec(3), Particle) @:disp("World Speed", "Speed in world space, added to the velocity without being accumulated") + public var partWorldSpeed : ParamVec3; + @:param(PVec(3), Particle) @:disp("Acceleration", "Acceleration in the emitter orientation, accumulated in the velocity") + public var partAcceleration : ParamVec3; + @:param(PVec(3), Particle) @:disp("World Acceleration", "Acceleration in world space (gravity, wind...), accumulated in the velocity") + public var partWorldAcceleration : ParamVec3; + @:param(PVec(3, -360, 360), Particle) @:disp("Orbit Speed", "Rotation speed around the emitter, in degrees per second") + public var partOrbitSpeed : ParamVec3; + @:s @:range(0, null) @:animate @:disp("Orbit Speed Multiplier", "Multiplier of the orbit speed of all the particles, animated over the emitter time") + public var orbitSpeedMultiplier : Float = 1; + + @:group("Limit Velocity") + @:param(PFloat(0), Particle) @:disp("Dampen", "Damping of the accumulated velocity (start speed and accelerations), per second") + public var partDampen : ParamMode; + @:param(PFloat(0), Particle) @:disp("Max Speed", "Maximum speed of the particle") + public var partMaxSpeed : ParamMode; + + @:group("Particle Transform") + @:param(PVec(3), Particle) @:disp("Offset", "Offset of the particle position, without effect on its movement") + public var partOffset : ParamVec3; + @:param(PVec(3), Particle) @:disp("Pivot", "Pivot of the particle, relative to its size") + public var partPivot : ParamVec3; + @:param(PVec(3, 0, 360), Particle) @:disp("Rotation", "Rotation of the particle, in degrees") + public var partRotation : ParamVec3; + @:param(PFloat(), Particle, 1.) @:disp("Scale", "Uniform scale of the particle") + public var partScale : ParamMode; + @:s @:range(0, null) @:animate @:disp("Scale Multiplier", "Multiplier of the scale of all the particles, animated over the emitter time") + public var scaleMultiplier : Float = 1; + @:param(PVec(3), Particle, [1., 1., 1.]) @:disp("Stretch", "Scale of the particle on each of its axes") + public var partStretch : ParamVec3; + @:param(PFloat(0), Particle) @:disp("Stretch By Velocity", "Stretches the particle along its velocity") + public var partStretchVelocity : ParamMode; + + @:group("Alignment") + @:s @:disp("Align Mode", "Alignment of the particles: none, facing the screen, around an axis, or along their speed") + public var alignMode : AlignMode = AlignMode.None; + @:s @:disp("Lock Axis", "Axis around which the particles rotate to face the camera") + public var alignLockAxis : AlignLockAxis = AlignLockAxis.ScreenZ; + @:s @:disp("Follow Rotation", "The aligned particles keep their own rotation and the one of the emitter") + public var followRotation : Bool = false; - // ANIMATION - @:param({ t: PFile(["jpg", "png"]), disp: "Sheet", group: "Sprite Sheet Animation" }) + @:group("Rendering") + @:s @:disp("Random Color", "Multiplies the color of each particle (alpha included) by a random color, taken between two colors or in a gradient") + public var randomColorMode : RandomColorMode = RandomColorMode.None; + @:s @:sameLine @:t(PColor) @:disp("Color 1", "First bound of the random color") + public var randomColor1 : Int = 0xFF000000; + @:s @:sameLine @:t(PColor) @:disp("Color 2", "Second bound of the random color") + public var randomColor2 : Int = 0xFFFFFFFF; + @:s @:sameLine @:disp("Gradient", "Gradient in which the random color is taken") + public var randomGradient : GradientData; + @:s @:disp("Fade Color", "Multiplies the color of each particle (alpha included) by a color faded over its life, from a color to another or along a gradient") + public var fadeColorMode : RandomColorMode = RandomColorMode.None; + @:s @:sameLine @:t(PColor) @:disp("Start Color", "Color at the birth of the particle") + public var fadeColor1 : Int = 0xFFFFFFFF; + @:s @:sameLine @:t(PColor) @:disp("End Color", "Color at the death of the particle") + public var fadeColor2 : Int = 0x00FFFFFF; + @:s @:sameLine @:disp("Gradient", "Gradient along which the color fades, over the life of the particle") + public var fadeGradient : GradientData; + @:s @:range(0, 1) @:line("Alpha Fade") @:disp("In", "Part of the particle life over which its alpha goes from 0 to 1, at its birth (0: none)") + public var fadeIn : Float = 0; + @:s @:range(0, 1) @:sameLine @:disp("Out", "Part of the particle life over which its alpha goes from 1 to 0, before its death (0: none)") + public var fadeOut : Float = 0; + + @:group("Sprite Sheet Animation") + @:s @:t(PFile(["jpg", "png"])) @:disp("Sheet", "Texture containing the frames of the animation") public var spriteSheet : String; - @:param({ t: PInt(0), disp: "Frames", group: "Sprite Sheet Animation" }) + @:s @:range(0) @:disp("Frames", "Number of frames, 0 for all the cells of the sheet") public var frameCount : Int = 0; - @:param({ t: PInt(1), disp: "Divisions X", group: "Sprite Sheet Animation" }) + @:s @:range(1) @:disp("Divisions X", "Number of columns of the sheet") public var frameDivisionX : Int = 1; - @:param({ t: PInt(1), disp: "Divisions Y", group: "Sprite Sheet Animation" }) + @:s @:range(1) @:disp("Divisions Y", "Number of rows of the sheet") public var frameDivisionY : Int = 1; - @:param({ t: PFloat(0, 2.0), disp: "Speed", group: "Sprite Sheet Animation" }) - public var animationSpeed : Float = 1.0; - @:param({ t: PBool, disp: "Loop", group: "Sprite Sheet Animation" }) + @:s @:range(0, 2.0) @:disp("Cycles", "Number of times the animation is played during the life of a particle") + public var animationCycles : Float = 1.0; + @:s @:disp("Loop", "Loops the animation, each particle starting on a random frame") public var animationLoop : Bool = true; - @:param({ t: PBool, disp: "Use Source UV", group: "Sprite Sheet Animation" }) + @:s @:disp("Use Source UV", "Keeps the UVs of the mesh inside each frame") public var animationUseSourceUVs : Bool = true; - @:param({ t: PBool, disp: "Blend frames", group: "Sprite Sheet Animation" }) + @:s @:disp("Blend Frames", "Blends two consecutive frames for a smooth animation") public var animationBlendBetweenFrames : Bool = true; - // COLLISION - @:param({ t: PBool, group: "Ground Collision" }) + @:group("Ground Collision") + @:s @:disp("Use Collision", "The particles bounce on the ground plane") public var useCollision : Bool = false; - @:param({ t: PFloat(0, 1.0), disp: "Elasticity", group: "Ground Collision" }) + @:s @:range(0, 1.0) @:disp("Elasticity", "Bounce of the particles: 0 makes them slide on the ground, 1 bounces them with their whole speed") public var elasticity : Float = 1.0; - @:param({ t: PFloat(0, 1.0), disp: "Kill On Collision", group: "Ground Collision" }) + @:s @:range(0, 1.0) @:disp("Kill Chance On Collision", "Probability for a particle to die when it touches the ground") public var killOnCollision : Float = 0.0; - // DEBUG - @:param({ t: PBool, disp: "Show Debug", group: "Debug" }) + @:group("Debug") + @:s @:disp("Show Debug", "Draws a cross at the position of each sub emitter") public var viewDebug : Bool = false; - // INSTANCES - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "World Acceleration", group: "Particle Movement" }) - public var instWorldAcceleration : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Fixed Speed", group: "Particle Movement" }) - public var instSpeed : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Fixed World Speed", group: "Particle Movement" }) - public var instWorldSpeed : Array; - - // In instance param to avoid refactoring the param editor more, but this is no longer linked to the instances (hence the instance: false in the declaration) - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Start Speed", group: "Particle Movement", instance: false }) - public var instStartSpeed : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Start World Speed", group: "Particle Movement", instance: false }) - public var instStartWorldSpeed : Array; - - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Orbit Speed", group: "Particle Movement" }) - public var instOrbitSpeed : Array; - @:instParam({ t: PFloat(0, 2.0), def: 1., disp: "Orbit Speed over time", group: "Particle Movement" }) - public var instOrbitSpeedOverTime : Null; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Acceleration", group: "Particle Movement" }) - public var instAcceleration : Array; - @:instParam({ t: PFloat(0, 10.0), def: 0., disp: "Max Velocity", group: "Limit Velocity" }) - public var instMaxVelocity : Null; - @:instParam({ t: PFloat(0, 10.0), def: 0., disp: "Dampen", group: "Limit Velocity" }) - public var instDampen : Null; - @:instParam({ t: PFloat(0, 2.0), def: 1., disp: "Scale", group: "Particle Transform" }) - public var instScale : Null; - @:instParam({ t: PFloat(0, 2.0), def: 1., disp: "Scale over time", group: "Particle Transform" }) - public var instScaleOverTime : Null; - @:instParam({ t: PVec(3, 0.0, 2.0), def: [1., 1., 1.], disp: "Stretch", group: "Particle Transform" }) - public var instStretch : Array; - @:instParam({ t: PFloat(0.0, 2.0), def: 0.0, disp: "Stretch Vel.", group: "Particle Transform" }) - public var instStretchVelocity : Null; - @:instParam({ t: PVec(3, 0, 360), def: [0., 0., 0.], disp: "Rotation", group: "Particle Transform" }) - public var instRotation : Array; - @:instParam({ t: PVec(3, -1, 1), def: [0., 0., 0.], disp: "Pivot", group: "Particle Transform" }) - public var instPivot : Array; - @:instParam({ t: PVec(3, -10, 10), def: [0., 0., 0.], disp: "Offset", group: "Particle Transform" }) - public var instOffset : Array; - public function new(parent, shared: ContextShared) { super(parent, shared); applyMeshTransform = true; // old emitters stay false, new ones true by default @@ -1914,21 +1840,31 @@ class Emitter extends Object3D { override function save() { var data = super.save(); - data.props = saveParams(); + saveParams(data); + if( randomColorMode != RandomColorMode.Gradient ) + Reflect.deleteField(data, "randomGradient"); + if( fadeColorMode != RandomColorMode.Gradient ) + Reflect.deleteField(data, "fadeGradient"); + if( !FX.inFXData(this) ) + data.version = FX.VERSION; return data; } override function load( obj : Dynamic ) { + // an emitter out of any FX (in a prefab) is the root of its FX data + if( !FX.inFXData(this) ) + FX.checkVersion(this, obj); super.load(obj); - loadParams(obj.props); - props = null; + loadParams(obj); // Negative warmup time is an old version of delay if( warmUpTime < 0 ) { delay = -warmUpTime; warmUpTime = 0; } - if( randomGradient == null ) - randomGradient = Gradient.getDefaultGradientData(); + if( randomGradient == null && randomColorMode == RandomColorMode.Gradient ) + randomGradient = hrt.impl.Gradient.getDefaultGradientData(); + if( fadeGradient == null && fadeColorMode == RandomColorMode.Gradient ) + fadeGradient = hrt.impl.Gradient.getDefaultGradientData(); } override function copy(obj:Prefab) { @@ -1952,7 +1888,6 @@ class Emitter extends Object3D { return; } - var randIdx = 0; var template : Object3D = cast children.find( c -> EmitterObject.checkEnabled(c) && (c.name == null || c.name.indexOf("collision") == -1) && @@ -1961,67 +1896,7 @@ class Emitter extends Object3D { c.to(Emitter) == null && c.to(hrt.prefab.l3d.Trails) == null); - var getCurve = hrt.prefab.Curve.getCurve.bind(this); - - function makeParam(base: Float, randProp: Null, pname: String, suffix: String) : Value { - var xVal = Evaluator.vVal(base); - var randCurve = getCurve(pname + suffix + ":rand"); - var randVal : Value = VZero; - if(randCurve != null) - randVal = Evaluator.vMult(VRandom(randIdx++, 1.0, 0.0), Evaluator.vMult(randCurve.makeVal(), VConst(randProp != null ? randProp : 1.0))); - else if(randProp != null && randProp != 0.0) - randVal = VRandom(randIdx++, randProp, 0.0); - - var xCurve = getCurve(pname + suffix); - if (xCurve != null) { - if (xCurve.blendMode == CurveBlendMode.RandomBlend) { - var ca = Std.downcast(xCurve.children[0], Curve); - var cb = Std.downcast(xCurve.children[1], Curve); - return VRandomBetweenCurves(randIdx++, ca, cb); - } - else { - if (pname.indexOf("Rotation") >= 0 || pname.indexOf("Offset") >= 0 || pname.indexOf("Pivot") >= 0) - return Evaluator.vAdd(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - else - return Evaluator.vMult(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - } - } - else - return Evaluator.vAdd(xVal, randVal); - } - - function fparam(p: hrt.prefab.fx.EmitterHelper.ParamDef, base: Null, rand: Null) : Value { - return makeParam(base != null ? base : p.def, rand, p.name, ""); - } - - function vparam(p: hrt.prefab.fx.EmitterHelper.ParamDef, base: Array, rand: Array) : Value { - var b : Array = base != null ? base : p.def; - inline function comp(idx, suffix) return makeParam(b[idx], rand != null ? rand[idx] : null, p.name, suffix); - var v : Value = VVector(comp(0, ":x"), comp(1, ":y"), comp(2, ":z")); - if(v.match(VVector(VZero, VZero, VZero))) - v = VZero; - else if(v.match(VVector(VConst(1.0), VConst(1.0), VConst(1.0)))) - v = VConst(1.0); - return v; - } - - var d = new InstanceDef(); - d.localSpeed = param(instSpeed); - d.worldSpeed = param(instWorldSpeed); - d.orbitSpeed = param(instOrbitSpeed); - d.orbitSpeedOverTime = param(instOrbitSpeedOverTime); - d.acceleration = param(instAcceleration); - d.worldAcceleration = param(instWorldAcceleration); - d.localOffset = param(instOffset); - d.scale = param(instScale); - d.scaleOverTime = param(instScaleOverTime); - d.dampen = param(instDampen); - d.maxVelocity = param(instMaxVelocity); - d.stretch = param(instStretch); - d.stretchVelocity = param(instStretchVelocity); - d.rotation = param(instRotation); - d.pivot = param(instPivot); - emitterObj.instDef = d; + emitterObj.evaluator = makeEvaluator(); emitterObj.particleTemplate = template; // SUB-EMITTER @@ -2032,77 +1907,15 @@ class Emitter extends Object3D { var trailsTemplate : hrt.prefab.l3d.Trails = cast children.find(p -> EmitterObject.checkEnabled(p) && Std.isOfType(p, hrt.prefab.l3d.Trails) && p.to(Object3D).visible); emitterObj.trailsTemplate = trailsTemplate; - // RANDOM - emitterObj.seedGroup = seedGroup; - emitterObj.subEmitterKind = subEmitterKind; - // LIFE - emitterObj.lifeTime = lifeTime; - emitterObj.lifeTimeRand = lifeTimeRand; - emitterObj.speedFactor = speedFactor; - emitterObj.warmUpTime = warmUpTime; - emitterObj.delay = delay; - // EMIT PARAMS - emitterObj.emitType = emitType; - emitterObj.burstCount = Std.int(burstCount); - emitterObj.burstDelay = burstDelay; - emitterObj.burstParticleCount = param(burstParticleCount); - emitterObj.emitDuration = emitDuration; - emitterObj.simulationSpace = simulationSpace; - emitterObj.particleScaling = particleScaling; - emitterObj.emitOrientation = emitOrientation; - emitterObj.maxCount = maxCount; - emitterObj.sortMode = enableSort ? sortMode : None; - emitterObj.emitRate = param(emitRate); - emitterObj.emitRateMin = param(emitRateMin); - emitterObj.emitRateMax = param(emitRateMax); - emitterObj.emitRateChangeDelay = emitRateChangeDelay; - emitterObj.emitShape = emitShape; - emitterObj.followRotation = followRotation; - emitterObj.applyMeshTransform = applyMeshTransform; - // EMIT SHAPE - emitterObj.emitAngle = param(emitAngle); - emitterObj.emitRad1 = emitRad1; - emitterObj.emitRad2 = emitRad2; - emitterObj.emitSurface = emitSurface; - emitterObj.emitDomeCut = emitDomeCut; - emitterObj.emitDomeFlip = emitDomeFlip; - // ALIGNMENT - emitterObj.alignMode = alignMode; - emitterObj.alignLockAxis = alignLockAxis; - // ANIMATION - emitterObj.spriteSheet = spriteSheet; - emitterObj.frameCount = frameCount; - emitterObj.frameDivisionX = frameDivisionX; - emitterObj.frameDivisionY = frameDivisionY; - emitterObj.animationSpeed = animationSpeed; - emitterObj.animationLoop = animationLoop; - emitterObj.animationUseSourceUVs = animationUseSourceUVs; - emitterObj.animationBlendBetweenFrames = animationBlendBetweenFrames; - - // COLLISION - emitterObj.useCollision = useCollision; - emitterObj.killOnCollision = killOnCollision; - emitterObj.elasticity = elasticity; - // RANDOM COLOR - emitterObj.useRandomColor = useRandomColor; - emitterObj.useRandomGradient = useRandomGradient; - emitterObj.randomColor1 = h3d.Vector.fromArray(randomColor1); - emitterObj.randomColor2 = h3d.Vector.fromArray(randomColor2); - emitterObj.randomGradient = randomGradient; - - // PARTICLE MOVEMENT - emitterObj.startSpeed = param(instStartSpeed); - emitterObj.startWorldSpeed = param(instStartWorldSpeed); - // DEBUG #if editor emitterObj.debugGraphics.visible = viewDebug; #end - emitterObj.init(randIdx, this); + emitterObj.init(this); #if editor - if(propName == null || ["emitShape", "emitAngle", "emitRad1", "emitRad2", "emitDomeCut", "emitDomeFlip"].indexOf(propName) >= 0) + if(propName == null || ["emitShape", "emitAngle", "emitRad1", "emitRad2", "emitDomeHeight", "emitDomeFlip"].indexOf(propName) >= 0) updateEmitShape(emitterObj); #end } @@ -2120,92 +1933,80 @@ class Emitter extends Object3D { return Rebuild; } - static var REFRESH_PARAMS = ["emitShape", "alignMode", "useCollision", "emitType", "useRandomColor", "useRandomGradient"]; + static var REFRESH_PARAMS = ["emitShape", "alignMode", "useCollision", "emitType", "randomColorMode", "fadeColorMode"]; - function getVisibleParams() : Array { - var params = emitterParams.copy(); - inline function removeParam(pname: String) { - params.remove(params.find(p -> p.name == pname)); - } + function getHiddenFields() : Array { + var hidden = []; + inline function hide(name: String) + hidden.push(name); if(!(emitShape == Cone || emitShape == Cylinder)) - removeParam("emitAngle"); + hide("emitAngle"); if(emitShape != Cylinder) { - removeParam("emitRad1"); - removeParam("emitRad2"); + hide("emitRad1"); + hide("emitRad2"); } if(emitShape != Dome) { - removeParam("emitDomeCut"); - removeParam("emitDomeFlip"); + hide("emitDomeHeight"); + hide("emitDomeFlip"); } + if(emitShape == Point || emitShape == Line) + hide("emitSurface"); switch(alignMode) { case None | Screen | Speed | ScreenSpeed | AxisSpeed: - removeParam("alignLockAxis"); + hide("alignLockAxis"); default: } var isSub = findParent(Emitter) != null; if (!isSub) { - removeParam("subEmitterKind"); + hide("subEmitterKind"); } if( !useCollision ) { - removeParam("elasticity"); - removeParam("killOnCollision"); + hide("elasticity"); + hide("killOnCollision"); } - if(!useRandomColor) { - removeParam("useRandomGradient"); - removeParam("randomColor1"); - removeParam("randomColor2"); - removeParam("randomGradient"); + if(randomColorMode != RandomColorMode.Colors) { + hide("randomColor1"); + hide("randomColor2"); } - else { - if (useRandomGradient) { - removeParam("randomColor1"); - removeParam("randomColor2"); - } else { - removeParam("randomGradient"); - } + if(randomColorMode != RandomColorMode.Gradient) + hide("randomGradient"); + if(fadeColorMode != RandomColorMode.Colors) { + hide("fadeColor1"); + hide("fadeColor2"); } + if(fadeColorMode != RandomColorMode.Gradient) + hide("fadeGradient"); switch (emitType) { case Infinity: - removeParam("burstCount"); - removeParam("burstDelay"); - removeParam("burstParticleCount"); - removeParam("emitDuration"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); - case InfinityRandom: - removeParam("emitRate"); - removeParam("burstCount"); - removeParam("burstDelay"); - removeParam("burstParticleCount"); - removeParam("emitDuration"); - case BurstDuration: - removeParam("emitRate"); - removeParam("burstCount"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); - case Burst: - removeParam("emitDuration"); - removeParam("emitRate"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + hide("burstCount"); + hide("burstInterval"); + hide("burstParticleCount"); + hide("emitDuration"); case Duration: - removeParam("burstCount"); - removeParam("burstDelay"); - removeParam("burstParticleCount"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + hide("burstCount"); + hide("burstInterval"); + hide("burstParticleCount"); + case Burst: + hide("emitDuration"); + hide("emitRate"); + case BurstDuration: + hide("burstCount"); + hide("emitRate"); + } + // read only by a random rate, which bursts do not use + var randomRate = switch (getMode("emitRate", 0)) { + case Range(_), RandomCurve(_), CurveRange: true; + case null, _: false; } - return params; + if (!randomRate || emitType == Burst || emitType == BurstDuration) + hide("emitRateChangeTime"); + return hidden; } #end @@ -2214,8 +2015,10 @@ class Emitter extends Object3D { super.edit2(ctx); function onChange(pname: String, isTemp: Bool) { - if (pname == "useRandomGradient") - randomGradient = useRandomGradient ? Gradient.getDefaultGradientData() : null; + if (pname == "randomColorMode") + randomGradient = randomColorMode == RandomColorMode.Gradient ? hrt.impl.Gradient.getDefaultGradientData() : null; + if (pname == "fadeColorMode") + fadeGradient = fadeColorMode == RandomColorMode.Gradient ? hrt.impl.Gradient.getDefaultGradientData() : null; if (pname == "warmUpTime" && warmUpTime < 0) { warmUpTime = 0; ctx.quickError("Warm up time can no longer be negative. Use the Delay property instead"); @@ -2225,127 +2028,11 @@ class Emitter extends Object3D { ctx.rebuildInspector(); } - var categories = new Map(); - function getCategory(name: String) { - var cat = categories.get(name); - if (cat == null) { - cat = new hide.kit.Category(ctx.root, name, name); - categories.set(name, cat); - } - return cat; - } - - function makeSlider(line: hide.kit.Line, name: String, label: String, value: Float, min: Null, max: Null, set: Float -> Void) { - var slider = new hide.kit.Slider(line, name); - slider.label = label; - slider.value = value; - slider.min = min; - slider.max = max; - @:privateAccess slider.showRange = min != null && max != null; - @:privateAccess slider.onFieldChange = (_) -> set(slider.value); - slider.onValueChange = (isTemp) -> onChange(name, isTemp); - } - - // Vectors are built here rather than with buildProp, which goes through Array - function addVecSliders(line: hide.kit.Line, name: String, n: Int, min: Null, max: Null) { - var vec : Array = Reflect.field(this, name); - for (i in 0...n) - makeSlider(line, name + "." + i, ["X", "Y", "Z", "W"][i], vec[i], min, max, (v) -> vec[i] = v); - } - - function addParam(p: hrt.prefab.fx.EmitterHelper.ParamDef) { - var cat = getCategory(p.groupName ?? "Emitter"); - switch (p.t) { - case PVec(n, min, max) if (p.name.toLowerCase().indexOf("color") < 0): - var line = new hide.kit.Line(cat, p.name); - line.label = p.disp ?? hide.kit.Macros.camelToSpaceCase(p.name); - addVecSliders(line, p.name, n, min, max); - default: - var widget = cat.buildProp(p, this); - if (widget != null) - widget.onValueChange = (isTemp) -> onChange(p.name, isTemp); - } - } - - for (p in getVisibleParams()) - addParam(p); - - // Instance params are optional (null means default), "+" sets them and "-" removes them - function addOptional(cat: hide.kit.Category, name: String, label: String, t: hrt.prefab.Props.PropType, init: () -> Dynamic) { - var line = new hide.kit.Line(cat, name); - line.label = label; - var set = Reflect.field(this, name) != null; - if (set) { - switch (t) { - case PVec(n, min, max): - addVecSliders(line, name, n, min, max); - case PFloat(min, max): - makeSlider(line, name, "", Reflect.field(this, name), min, max, (v) -> Reflect.setField(this, name, v)); - default: - throw "Unsupported instance param type " + t; - } - } - var btn = new hide.kit.Button(line, set ? "remove" : "add", set ? "-" : "+"); - btn.width = 1; - btn.onClick = () -> { - Reflect.setField(this, name, set ? null : init()); - ctx.rebuildPrefab(this); - ctx.rebuildInspector(); - } - } - - for (p in instanceParams) { - var cat = getCategory(p.groupName ?? "Particles"); - addOptional(cat, p.name, p.disp ?? hide.kit.Macros.camelToSpaceCase(p.name), p.t, function() : Dynamic return switch (p.t) { - case PVec(_): ((p.def : Array).copy() : Dynamic); - default: p.def; - }); - addOptional(cat, hrt.prefab.fx.EmitterHelper.randProp(p.name), "~", p.t, function() : Dynamic return switch (p.t) { - case PVec(n): ([for (_ in 0...n) 0.0] : Dynamic); - default: (0.0 : Dynamic); - }); - } + EmitterHelper.edit(ctx, this, getHiddenFields(), onChange); } #if editor - override function edit( ctx : hide.prefab.EditContext ) { - super.edit(ctx); - - function refresh() { - ctx.rebuildProperties(); - } - - function onChange(?pname: String) { - if (pname == "useRandomGradient") { - randomGradient = useRandomGradient ? Gradient.getDefaultGradientData() : null; - refresh(); - } - - ctx.onChange(this, pname); - - if (pname == "warmUpTime") { - if (warmUpTime < 0) { - warmUpTime = 0; - hide.Ide.inst.quickError("Warm up time can no longer be negative. Use the Delay property instead"); - refresh(); - } - } - - if(["emitShape", - "alignMode", - "useCollision", - "emitType", - "useRandomColor", - ].indexOf(pname) >= 0) { - refresh(); - } - } - - var params = getVisibleParams(); - EmitterHelper.generateEdit(params, instanceParams, this, ctx.properties, onChange, refresh, true); - } - override function setSelected(b : Bool ) { var emitterObj = Std.downcast(local3d, EmitterObject); if(emitterObj == null) @@ -2383,7 +2070,7 @@ class Emitter extends Object3D { debugShape.removeChildren(); var mesh : h3d.scene.Mesh = null; - switch(emitterObj.emitShape) { + switch(emitShape) { case Cylinder: { var rad1 = emitRad1 * 0.5; var rad2 = emitRad2 * 0.5; @@ -2456,8 +2143,23 @@ class Emitter extends Object3D { } case Sphere: mesh = new h3d.scene.Sphere(0xffffff, 0.5, true, debugShape); + case Point, Line: { + var g = new h3d.scene.Graphics(debugShape); + g.material.mainPass.setPassName("overlay"); + g.lineStyle(1, 0xffffff); + g.moveTo(-0.5, 0, 0); + g.lineTo(0.5, 0, 0); + if(emitShape == Point) { + g.moveTo(0, -0.5, 0); + g.lineTo(0, 0.5, 0); + g.moveTo(0, 0, -0.5); + g.lineTo(0, 0, 0.5); + } + g.ignoreCollide = true; + mesh = g; + } case Dome: { - var maxPhi = Math.acos(1.0 - 2.0 * hxd.Math.clamp(emitDomeCut, 0, 1)); + var maxPhi = Math.acos(1.0 - 2.0 * hxd.Math.clamp(emitDomeHeight, 0, 1)); var sz = emitDomeFlip ? -0.5 : 0.5; var g = new h3d.scene.Graphics(debugShape); inline function pt(phi : Float, t : Float, first : Bool) { diff --git a/hrt/prefab/fx/Emitter2D.hx b/hrt/prefab/fx/Emitter2D.hx index 266a6389c..8b91aea69 100644 --- a/hrt/prefab/fx/Emitter2D.hx +++ b/hrt/prefab/fx/Emitter2D.hx @@ -1,8 +1,9 @@ package hrt.prefab.fx; import hrt.prefab.fx.Emitter; +import hrt.prefab.fx.Value; +import hrt.prefab.fx.EmitterParam; import hrt.impl.Gradient; -import hrt.prefab.Curve; using Lambda; @@ -74,42 +75,39 @@ class Particle2DInstance extends h2d.SpriteBatch.BatchElement { } function getInitialAbsPos(m : h2d.col.Matrix, dt : Float) { - var def = emitter.instDef; var evaluator = @:privateAccess emitter.evaluator; var t = hxd.Math.clamp(curLifeTime / lifeTime); m.identity(); // POSITION - if (def.localOffset != VZero) { - evaluator.getVector2(idx, def.localOffset, t, tmpVec); + if(evaluator.getPartOffset(idx, t, tmpVec)) { m.initTranslate(tmpVec.x, tmpVec.y); } // ROTATION - if (def.rotation != VZero) { - var r = evaluator.getFloat(idx, def.rotation, t) * Math.PI / 180.0; + if (evaluator.partRotation != VZero) { + var r = evaluator.getPartRotation(idx, t) * Math.PI / 180.0; m.rotate(r); } // SCALE - evaluator.getVector2(idx, def.stretch, t, tmpVec); - tmpVec *= evaluator.getFloat(idx, def.scale, t); - tmpVec *= evaluator.getFloat(idx, def.scaleOverTime, emitter.curTime); + evaluator.getPartStretch(idx, t, tmpVec); + tmpVec *= evaluator.getPartScale(idx, t); + tmpVec *= emitter.curScaleMultiplier; m.scale(tmpVec.x, tmpVec.y); } function getCurrentAbsPos(m : h2d.col.Matrix, dt : Float) { - var def = emitter.instDef; var evaluator = @:privateAccess emitter.evaluator; var t = hxd.Math.clamp(curLifeTime / lifeTime); if (t == 0) { - switch (emitter.emitShape) { + switch (emitter.prefab.emitShape) { case Circle: - var radius = evaluator.getFloat(emitter.emitRadius, emitter.curTime); - var startAngle = evaluator.getFloat(emitter.emitAngle1, emitter.curTime) * hxd.Math.PI / 180; - var endAngle = evaluator.getFloat(emitter.emitAngle2, emitter.curTime) * hxd.Math.PI / 180; + var radius = evaluator.getEmitRadius(emitter.curTime); + var startAngle = evaluator.getEmitStartAngle(emitter.curTime) * hxd.Math.PI / 180; + var endAngle = evaluator.getEmitEndAngle(emitter.curTime) * hxd.Math.PI / 180; // normalize the angular span to [0, 2PI) var twoPI = Math.PI * 2; @@ -125,56 +123,54 @@ class Particle2DInstance extends h2d.SpriteBatch.BatchElement { // pick radius so area is uniform: r = sqrt(u*(R^2 - r0^2) + r0^2) var rsq = emitter.rand.rand() * Math.max(0, radius) * Math.max(0, radius); - var r = emitter.emitSurface ? radius : Math.sqrt(rsq); + var r = emitter.prefab.emitSurface ? radius : Math.sqrt(rsq); var posX = r * Math.cos(a); var posY = r * Math.sin(a); - if (emitter.emitOrientation.match(Normal)) + if (emitter.prefab.emitOrientation.match(Normal)) m.initRotate(new Vector(posX, posY).getRotation()); m.translate(posX, posY); case Rectangle: - var width = evaluator.getFloat(emitter.emitWidth, emitter.curTime); - var height = evaluator.getFloat(emitter.emitHeight, emitter.curTime); + var width = evaluator.getEmitWidth(emitter.curTime); + var height = evaluator.getEmitHeight(emitter.curTime); var posX = (emitter.rand.rand() - 0.5) * width; var posY = (emitter.rand.rand() - 0.5) * height; - if (emitter.emitSurface) { + if (emitter.prefab.emitSurface) { if (emitter.rand.rand() < 0.5) posX = emitter.rand.rand() < 0.5 ? -width / 2 : width / 2; else posY = emitter.rand.rand() < 0.5 ? -height / 2 : height / 2; } - if (emitter.emitOrientation.match(Normal)) + if (emitter.prefab.emitOrientation.match(Normal)) m.initRotate(new Vector(posX, posY).getRotation()); m.translate(posX, posY); } // START LOCAL SPEED - evaluator.getVector2(idx, emitter.startSpeed, emitter.curTime, tmpVec); + evaluator.getStartSpeed(idx, emitter.curTime, tmpVec); tmpVec.transform2x2(m); speed += tmpVec; // START WORLD SPEED - evaluator.getVector2(idx, emitter.startWorldSpeed, emitter.curTime, tmpVec); + evaluator.getStartWorldSpeed(idx, emitter.curTime, tmpVec); tmpVec.transform2x2(emitter.invTransform); speed += tmpVec; } // ACCELERATION - if (def.acceleration != VZero) { - evaluator.getVector2(idx, def.acceleration, t, tmpVec); + if(evaluator.getPartAcceleration(idx, t, tmpVec)) { tmpVec.scale(dt); tmpVec.transform2x2(m); speed += tmpVec; } // WORLD ACCELERATION - if (def.worldAcceleration != VZero) { - evaluator.getVector2(idx, def.worldAcceleration, t, tmpVec); + if(evaluator.getPartWorldAcceleration(idx, t, tmpVec)) { tmpVec.scale(dt); tmpVec.transform2x2(emitter.invTransform); speed += tmpVec; @@ -183,30 +179,28 @@ class Particle2DInstance extends h2d.SpriteBatch.BatchElement { tmpSpeed.set(speed.x, speed.y); // SPEED - if (def.localSpeed != VZero) { - evaluator.getVector2(idx, def.localSpeed, t, tmpVec); + if(evaluator.getPartSpeed(idx, t, tmpVec)) { tmpVec.transform2x2(m); tmpSpeed += tmpVec; } // DAMPEN - if (def.dampen != VZero) { - var dampen = evaluator.getFloat(idx, def.dampen, t); + if (evaluator.partDampen != VZero) { + var dampen = evaluator.getPartDampen(idx, t); var scale = Math.exp(dampen* -dt); speed.scale(scale); } // WORLD SPEED - if (def.worldSpeed != VZero) { - evaluator.getVector2(idx, def.worldSpeed, t, tmpVec); + if(evaluator.getPartWorldSpeed(idx, t, tmpVec)) { tmpVec.transform2x2(emitter.invTransform); tmpSpeed += tmpVec; } // MAX VELOCITY - if (def.maxVelocity != VZero) { - var maxVel = evaluator.getFloat(idx, def.maxVelocity, t); + if (evaluator.partMaxSpeed != VZero) { + var maxVel = evaluator.getPartMaxSpeed(idx, t); var curVelSq = tmpSpeed.lengthSq(); if (maxVel * maxVel < curVelSq) { tmpSpeed.normalize(); @@ -216,11 +210,11 @@ class Particle2DInstance extends h2d.SpriteBatch.BatchElement { m.translate(tmpSpeed.x * dt, tmpSpeed.y * dt); - if (def.orbitSpeed != VZero) { - var orbitSpeed = evaluator.getFloat(idx, def.orbitSpeed, t); + if (evaluator.partOrbitSpeed != VZero) { + var orbitSpeed = evaluator.getPartOrbitSpeed(idx, t); - var factorOverTime = evaluator.getFloat(idx, def.orbitSpeedOverTime, emitter.curTime); - orbitSpeed *= factorOverTime; + var factorOverTime = emitter.curOrbitSpeedMultiplier; + orbitSpeed *= factorOverTime * (Math.PI / 180.0); var prevPos = m.getPosition().clone(); tmpMat.initRotate(orbitSpeed * dt); @@ -234,73 +228,40 @@ class Particle2DInstance extends h2d.SpriteBatch.BatchElement { tmpSpeed.y += delta.y; } - if (emitter.emitOrientation.match(Speed) && hxd.Math.abs(tmpSpeed.length()) > 0.0001) { + if (emitter.prefab.emitOrientation.match(Speed) && hxd.Math.abs(tmpSpeed.length()) > 0.0001) { var targetRotation = tmpSpeed.getRotation(); basisAbsPos.rotate(targetRotation); } } } +@:build(hrt.prefab.fx.EmitterMacros.buildEvaluator("hrt.prefab.fx.Emitter2D")) +class Emitter2DEvaluator extends Evaluator {} + class Emitter2DObject extends h2d.Object { public var particles : Array = []; public var batch : h2d.SpriteBatch; - var evaluator : Evaluator; + // built by the prefab, with a Value per param and animated property (EmitterMacros) + public var evaluator : Emitter2DEvaluator; var rand : hxd.Rand; var prevTime : Float; var curTime = 0.; + // the values of the over time settings at the current tick, shared by the particles + var curScaleMultiplier = 1.; + var curOrbitSpeedMultiplier = 1.; var countToEmit = 0.; var totalBurstCount = 0; - var randomValues : Array; - var randSlots : Int; var instanceCounter = 0; var animatedTexShader : h3d.shader.AnimatedTexture = null; - // Emitter parameters - public var instDef : InstanceDef; - public var seed = 0; - public var lifeTime = 2.0; - public var lifeTimeRand = 0.0; - public var speedFactor = 1.0; - public var warmUpTime = 0.0; - public var delay = 0.0; - public var emitOrientation : Orientation = Forward; - public var emitType : EmitType = Infinity; - public var burstCount : Int = 1; - public var burstParticleCount : Value; - public var burstDelay : Float = 1.0; - public var emitDuration : Float = 1.0; - public var emitRate : Value; - public var emitRateMin : Value; - public var emitRateMax : Value; - public var emitRateChangeDelay : Float = 1.0; + public var prefab : Emitter2D; + + // Game overrides public var emitScale : Float = 1.0; - public var maxCount = 20; - public var startSpeed: Value; - public var startWorldSpeed: Value; - public var emitShape : Emit2DShape = Circle; - public var emitRadius : Value; - public var emitAngle1 : Value; - public var emitAngle2 : Value; - public var emitWidth : Value; - public var emitHeight : Value; - public var emitSurface : Bool = false; - public var spriteType : SpriteType = Color; - public var color : h3d.Vector; - public var texture : String; - public var spriteSheet : String; - public var frameCount : Int = 0; - public var frameDivisionX : Int = 1; - public var frameDivisionY : Int = 1; - public var animationSpeed : Float = 1; - public var animationLoop : Bool = true; - public var animationUseSourceUVs : Bool = true; - public var animationBlendBetweenFrames : Bool = true; - public var useRandomColor : Bool = false; - public var useRandomGradient : Bool = false; - public var randomColor1 : h3d.Vector; - public var randomColor2 : h3d.Vector; - public var randomGradient : GradientData; + public var lifeTimeScale : Float = 1.0; + + public var invTransform = new h2d.col.Matrix(); public function new(?parent) { @@ -308,24 +269,25 @@ class Emitter2DObject extends h2d.Object { batch = new h2d.SpriteBatch(h2d.Tile.fromColor(0xFF00FF, 10, 10), this); batch.hasRotationScale = true; - evaluator = new Evaluator([0], 1); - rand = new hxd.Rand(seed); + rand = new hxd.Rand(0); } - public function init(randSlots: Int, prefab: Emitter2D) { - var randomValues = [for(_ in 0...(maxCount * randSlots)) rand.srand()]; - evaluator = new Evaluator(randomValues, randSlots); + public function init(prefab: Emitter2D) { + this.prefab = prefab; + evaluator.allocRandom(prefab.maxCount, evaluator.randomSlots); + evaluator.resetRandom(rand); if (animatedTexShader != null) batch.removeShader(animatedTexShader); - if (spriteSheet != null) { - var tex = hxd.res.Loader.currentInstance.load(spriteSheet).toTexture(); - animatedTexShader = new h3d.shader.AnimatedTexture(tex, frameDivisionX, frameDivisionY, frameCount, frameCount * animationSpeed / lifeTime); - animatedTexShader.useSourceUVs = animationUseSourceUVs; + if (prefab.spriteSheet != null) { + var tex = hxd.res.Loader.currentInstance.load(prefab.spriteSheet).toTexture(); + var meanLifeTime = evaluator.getMean(evaluator.lifeTime, 0); + animatedTexShader = new h3d.shader.AnimatedTexture(tex, prefab.frameDivisionX, prefab.frameDivisionY, prefab.frameCount, prefab.frameCount * prefab.animationCycles / meanLifeTime); + animatedTexShader.useSourceUVs = prefab.animationUseSourceUVs; animatedTexShader.startTime = 0; - animatedTexShader.loop = animationLoop; - animatedTexShader.blendBetweenFrames = animationBlendBetweenFrames; + animatedTexShader.loop = prefab.animationLoop; + animatedTexShader.blendBetweenFrames = prefab.animationBlendBetweenFrames; animatedTexShader.setPriority(1); batch.addShader(animatedTexShader); } @@ -333,7 +295,7 @@ class Emitter2DObject extends h2d.Object { public function setTime(time : Float, inCatchup : Bool = false) { prevTime = curTime; - curTime = (time * speedFactor + warmUpTime) - delay; + curTime = (time * prefab.timeScale + prefab.warmUpTime) - prefab.delay; if (curTime < 0) { if (prevTime > 0) @@ -350,7 +312,7 @@ class Emitter2DObject extends h2d.Object { var t = 0.; while (curTime < targetTime) { t = hxd.Math.min(t + hxd.Timer.dt, targetTime); - setTime(t - warmUpTime + delay, true); + setTime(t - prefab.warmUpTime + prefab.delay, true); } } else if (dt > 0) { @@ -358,39 +320,48 @@ class Emitter2DObject extends h2d.Object { } } + inline function getBurstCount() { + return EmitterHelper.getBurstCount(evaluator, evaluator.burstParticleCount, curTime, totalBurstCount, prefab.seed); + } + + inline function getRate() { + return EmitterHelper.getRate(evaluator, evaluator.emitRate, curTime, prefab.emitRateChangeTime, prefab.seed); + } + function tick(dt : Float) { - switch (emitType) { + curScaleMultiplier = evaluator.getScaleMultiplier(curTime); + curOrbitSpeedMultiplier = evaluator.getOrbitSpeedMultiplier(curTime); + + switch (prefab.emitType) { case Infinity: - var emitTarget = evaluator.getFloat(emitRate, curTime) * hxd.Math.clamp(emitScale) * dt; - countToEmit += hxd.Math.min(maxCount - particles.length, emitTarget); + var emitTarget = getRate() * hxd.Math.clamp(emitScale) * dt; + countToEmit += hxd.Math.min(prefab.maxCount - particles.length, emitTarget); var t = hxd.Math.floor(countToEmit); countToEmit -= t; doEmit(t); - case InfinityRandom: - // TODO - case Duration: - var emitTarget = evaluator.getFloat(emitRate, curTime) * hxd.Math.clamp(emitScale) * dt; - countToEmit += curTime >= emitDuration ? 0 : hxd.Math.min(maxCount - particles.length, emitTarget); + var emitTarget = getRate() * hxd.Math.clamp(emitScale) * dt; + countToEmit += curTime >= prefab.emitDuration ? 0 : hxd.Math.min(prefab.maxCount - particles.length, emitTarget); var t = hxd.Math.floor(countToEmit); countToEmit -= t; doEmit(t); case Burst: - if (burstDelay > 0) { - var burstTarget = hxd.Math.min(burstCount, 1 + hxd.Math.floor(curTime / burstDelay)); + if (prefab.burstInterval > 0) { + var burstTarget = hxd.Math.min(prefab.burstCount, 1 + hxd.Math.floor(curTime / prefab.burstInterval)); while (totalBurstCount < burstTarget) { - var emitTarget = hxd.Math.ceil(hxd.Math.min(maxCount - particles.length, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); + var emitTarget = hxd.Math.ceil(hxd.Math.min(prefab.maxCount - particles.length, getBurstCount())); doEmit(emitTarget); totalBurstCount++; } } case BurstDuration: - if (curTime < emitDuration) { - var emitTarget = hxd.Math.ceil(hxd.Math.min(maxCount - particles.length, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); + if (curTime < prefab.emitDuration) { + var emitTarget = hxd.Math.ceil(hxd.Math.min(prefab.maxCount - particles.length, getBurstCount())); doEmit(emitTarget); + totalBurstCount++; } } @@ -412,10 +383,10 @@ class Emitter2DObject extends h2d.Object { function doEmit(count : Int) { for (_ in 0...count) { var part = new Particle2DInstance(getTile(), this); - part.lifeTime = lifeTime + rand.srand(lifeTimeRand); part.idx = instanceCounter; + part.lifeTime = evaluator.getLifeTime(part.idx, curTime) * lifeTimeScale; this.particles.push(part); - instanceCounter = (instanceCounter + 1) % maxCount; + instanceCounter = (instanceCounter + 1) % prefab.maxCount; } } @@ -426,29 +397,30 @@ class Emitter2DObject extends h2d.Object { totalBurstCount = 0; instanceCounter = 0; - rand.init(seed); - if (randomValues != null) { - for(i in 0...randomValues.length) - randomValues[i] = rand.srand(); - } + rand.init(prefab.seed); + evaluator.resetRandom(rand); + } + + static function colorTile( c : Int ) { + return h2d.Tile.fromColor(c, 10, 10, (c >>> 24) / 255); } function getTile() { var t = h2d.Tile.fromColor(0xFF00FF, 10, 10); - switch (spriteType) { + switch (prefab.spriteType) { case Color: - if (!useRandomColor && !useRandomGradient) - t = h2d.Tile.fromColor(color.toColor(), 10, 10); - else if (useRandomColor && !useRandomGradient) { - var c = new h3d.Vector(); - c.lerp(randomColor1, randomColor2, rand.rand()); - t = h2d.Tile.fromColor(c.toColor(), 10, 10); - } - else if (useRandomGradient) { - t = h2d.Tile.fromColor(Gradient.evalData(randomGradient, rand.rand()).toColor(), 10, 10); + switch (prefab.randomColorMode) { + case None: + t = colorTile(prefab.color); + case Colors: + var c = h3d.Vector4.fromColor(prefab.randomColor1); + c.lerp(c, h3d.Vector4.fromColor(prefab.randomColor2), rand.rand()); + t = colorTile(c.toColor()); + case Gradient: + t = colorTile(hrt.impl.Gradient.evalData(prefab.randomGradient, rand.rand()).toColor()); } case Texture: - t = h2d.Tile.fromTexture(hxd.res.Loader.currentInstance.load(texture).toTexture()); + t = h2d.Tile.fromTexture(hxd.res.Loader.currentInstance.load(prefab.texture).toTexture()); default: } @@ -461,172 +433,154 @@ class Emitter2DObject extends h2d.Object { @:access(hrt.prefab.fx.Emitter2DObject) @:prefabName("Emitter 2D") @:prefabIcon(hrt.ui.HuiRes.ui.icons.prefab.emitter2d) -class Emitter2D extends Object2D { - - public static var emitterParams : Array = [ - // PROPERTIES - { name: "speedFactor", disp: "Speed Factor", t: PFloat(0, 1), def: 1.0, groupName : "Properties" }, - { name: "seed", t: PInt(0, 100), def: 0, groupName : "Properties", disp: "Seed"}, - - // EMISSION - { name: "warmUpTime", disp: "Warm Up", t: PFloat(0, 1), def: 0.0, groupName : "Emission" }, - { name: "delay", disp: "Delay", t: PFloat(0, 10), def: 0.0, groupName : "Emission" }, - { name: "emitType", t: PEnum(EmitType), def: EmitType.Infinity, disp: "Type", groupName : "Emission" }, - { name: "emitDuration", t: PFloat(0, 10.0), disp: "Duration", def : 1.0, groupName : "Emission" }, - { name: "emitRate", t: PInt(0, 100), def: 5, disp: "Rate", animate: true, groupName : "Emission" }, - { name: "emitRateMin", t: PInt(0, 100), def: 5, disp: "Rate Min", animate: true, groupName : "Emission" }, - { name: "emitRateMax", t: PInt(0, 100), def: 5, disp: "Rate Max", animate: true, groupName : "Emission" }, - { name: "emitRateChangeDelay", t: PFloat(0.01, 5.0), def: 1.0, disp: "Rate Change Time", groupName : "Emission" }, - { name: "burstCount", t: PInt(1, 10), disp: "Count", def : 1, groupName : "Emission" }, - { name: "burstDelay", t: PFloat(0, 1.0), disp: "Delay", def : 1.0, groupName : "Emission" }, - { name: "burstParticleCount", t: PInt(1, 10), disp: "Particle Count", def : 1, groupName : "Emission", animate: true }, - { name: "maxCount", t: PInt(0, 100), def: 20, groupName : "Emission" }, - { name: "emitOrientation", t: PEnum(Orientation), def: Orientation.Forward, disp: "Orientation", groupName : "Emission" }, - - // SHAPE EMISSION - { name: "emitShape", t: PEnum(Emit2DShape), def: Emit2DShape.Circle, disp: "Shape", groupName : "Shape Emission" }, - { name: "emitRadius", t: PFloat(0, 360.0), def: 20.0, disp: "Radius", groupName : "Shape Emission", animate: true }, - { name: "emitAngle1", t: PFloat(0, 360.0), def: 20.0, disp: "Angle 1", groupName : "Shape Emission", animate: true }, - { name: "emitAngle2", t: PFloat(0, 360.0), def: 40.0, disp: "Angle 2", groupName : "Shape Emission", animate: true }, - { name: "emitWidth", t: PFloat(0, 10.0), def: 1.0, disp: "Width", groupName : "Shape Emission", animate: true }, - { name: "emitHeight", t: PFloat(0, 10.0), def: 1.0, disp: "Height", groupName : "Shape Emission", animate: true }, - { name: "emitSurface", t: PBool, def: false, disp: "Surface", groupName : "Shape Emission" }, - - // PARTICLE - { name: "lifeTime", t: PFloat(0, 10), def: 1.0, groupName : "Particle" }, - { name: "lifeTimeRand", t: PFloat(0, 1), def: 0.0, groupName : "Particle" }, - { name: "spriteType", t: PEnum(SpriteType), def: SpriteType.Color, groupName : "Particle" }, - { name: "color", t: PVec(4), def: [0,0,0,1], groupName : "Particle" }, - { name: "texture", t: PTexture, def: "", groupName : "Particle" }, - { name: "useRandomColor", t: PBool, def: false, disp: "Random Color", groupName : "Particle" }, - { name: "useRandomGradient", t: PBool, def: false, disp: "Random Gradient", groupName : "Particle" }, - { name: "randomColor1", t: PVec(4), disp: "Color 1", def : [0,0,0,1], groupName : "Particle" }, - { name: "randomColor2", t: PVec(4), disp: "Color 2", def : [1,1,1,1], groupName : "Particle" }, - { name: "randomGradient", t:PGradient, disp: "Gradient", def: null, groupName : "Particle" }, - { name: "spriteSheet", t: PFile(["jpg","png"]), def: null, groupName : "Particle", disp: "Sheet" }, - { name: "frameCount", t: PInt(0), def: 0, groupName : "Particle", disp: "Frames" }, - { name: "frameDivisionX", t: PInt(1), def: 1, groupName : "Particle", disp: "Divisions X" }, - { name: "frameDivisionY", t: PInt(1), def: 1, groupName : "Particle", disp: "Divisions Y" }, - { name: "animationSpeed", t: PFloat(0, 2.0), def: 1.0, groupName : "Particle", disp: "Speed" }, - { name: "animationLoop", t: PBool, def: true, groupName : "Particle", disp: "Loop" }, - { name: "animationUseSourceUVs", t: PBool, def: true, groupName : "Particle", disp: "Use Source UV" }, - { name: "animationBlendBetweenFrames", t: PBool, def: true, groupName : "Particle", disp: "Blend frames" }, - ]; - - public static var instanceParams : Array = [ - // PARTICLE TRANSFORM - { name: "instScale", t: PFloat(0, 2.0), def: 1., disp: "Scale", groupName: "Particle Transform"}, - { name: "instScaleOverTime", t: PFloat(0, 2.0), def: 1., disp: "Scale over time", groupName: "Particle Transform"}, - { name: "instStretch", t: PVec(2, 0.0, 2.0), def: [1.,1.], disp: "Stretch", groupName: "Particle Transform"}, - { name: "instRotation", t: PFloat(0, 360), def: 0., disp: "Rotation", groupName: "Particle Transform"}, - { name: "instOffset", t: PVec(2, -10, 10), def: [0.,0.], disp: "Offset", groupName: "Particle Transform"}, - - // PARTICLE MOVEMENT - { name: "instAcceleration", t: PVec(2, -10, 10), def: [0.,0.], disp: "Acceleration", groupName: "Particle Movement"}, - { name: "instWorldAcceleration", t: PVec(2, -10, 10), def: [0.,0.], disp: "World Acceleration", groupName: "Particle Movement"}, - { name: "instSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Fixed Speed", groupName: "Particle Movement" }, - { name: "instWorldSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Fixed World Speed", groupName: "Particle Movement"}, - // In instance param to avoid refactoring the param editor more, but this is no longer linked to the instances (hence the instance: false in the declaration) - { name: "instStartSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Start Speed", groupName: "Particle Movement", instance: false}, - { name: "instStartWorldSpeed", t: PVec(2, -10, 10), def: [0.,0.], disp: "Start World Speed", groupName: "Particle Movement", instance: false}, - { name: "instOrbitSpeed", t: PFloat(-10., 10.), def: 0., disp: "Orbit Speed", groupName: "Particle Movement"}, - { name: "instOrbitSpeedOverTime", t: PFloat(0, 2.0), def: 1., disp: "Orbit Speed over time", groupName: "Particle Movement"}, - - // PARTICLE LIMIT VELOCITY - { name: "instMaxVelocity", t: PFloat(0, 10.0), def: 0., disp: "Max Velocity", groupName: "Particle Limit Velocity"}, - { name: "instDampen", t: PFloat(0, 10.0), def: 0., disp: "Dampen", groupName: "Particle Limit Velocity"}, - ]; - - public static var PARAMS : Map = { - var map = new Map(); - for(e in emitterParams) { - map.set(e.name, e); - } - for(i in instanceParams) { - i.instance = i.instance ?? true; - i.animate = true; - map.set(i.name, i); - } - map; - }; +@:build(hrt.prefab.fx.EmitterMacros.build("hrt.prefab.fx.Emitter2D.Emitter2DEvaluator")) +class Emitter2D extends Object2D implements hrt.prefab.fx.EmitterParam.IEmitter { + + @:group("Properties") + @:s @:range(0, 1) @:disp("Time Scale", "Time multiplier of the simulation") + public var timeScale : Float = 1.0; + @:s @:range(0, 10) @:disp("Start Delay", "Time before the start of the emitter, in seconds") + public var delay : Float = 0.0; + @:s @:range(0, 1) @:disp("Warm Up", "Time simulated before the start, in seconds: the emitter starts already filled") + public var warmUpTime : Float = 0.0; + @:s @:range(0, 100) @:disp("Seed", "Seed of the random values of the emitter") + public var seed : Int = 0; + + @:group("Emission") + @:s @:disp("Type", "Infinity: emits at its rate. Duration: emits at its rate during a duration. Burst / Burst Duration: groups of particles at regular intervals") + public var emitType : EmitType = EmitType.Infinity; + @:s @:range(0, 10.0) @:disp("Duration", "Emission duration, in seconds") + public var emitDuration : Float = 1.0; + @:param(PFloat(0, 100), Emission, 5.) @:disp("Rate", "Number of particles emitted per second. A Range changes it at random between min and max, every Rate Change Time") + public var emitRate : ParamMode; + @:s @:range(0.01, 5.0) @:disp("Rate Change Time", "Random rate: every this many seconds a new random rate is picked between min and max, and the rate goes to it during this time") + public var emitRateChangeTime : Float = 1.0; + @:s @:range(1, 10) @:disp("Burst Count", "Number of bursts") + public var burstCount : Int = 1; + @:s @:range(0, 1.0) @:disp("Burst Interval", "Time between two bursts, in seconds") + public var burstInterval : Float = 1.0; + @:param(PFloat(1, 10), Emission, 1.) @:disp("Particles Per Burst", "Number of particles emitted by each burst. A Range draws it for each burst, between min and max") + public var burstParticleCount : ParamMode; + @:s @:range(0, 100) @:disp("Max Count", "Maximum number of particles alive at the same time: no particle is emitted beyond") + public var maxCount : Int = 20; + @:param(PFloat(0, 10), Spawn, 1.) @:disp("Lifetime", "Life duration of the particle, in seconds") + public var lifeTime : ParamMode; + + @:group("Emit Shape") + @:s @:disp("Shape", "Area in which the particles are emitted") + public var emitShape : Emit2DShape = Emit2DShape.Circle; + @:s @:range(0, 360.0) @:animate @:disp("Radius", "Radius of the circle") + public var emitRadius : Float = 20.0; + @:s @:range(0, 360.0) @:animate @:disp("Start Angle", "Start angle of the circle arc, in degrees") + public var emitStartAngle : Float = 20.0; + @:s @:range(0, 360.0) @:animate @:disp("End Angle", "End angle of the circle arc, in degrees") + public var emitEndAngle : Float = 40.0; + @:s @:range(0, 10.0) @:animate @:disp("Width", "Width of the rectangle") + public var emitWidth : Float = 1.0; + @:s @:range(0, 10.0) @:animate @:disp("Height", "Height of the rectangle") + public var emitHeight : Float = 1.0; + @:s @:disp("Surface", "Emits on the outline of the shape instead of inside it") + public var emitSurface : Bool = false; + @:s @:disp("Orientation", "Initial orientation of the particles: the emitter one, the normal of the shape, their speed, or random") + public var emitOrientation : Orientation = Orientation.Forward; + + @:group("Particle Movement") + @:param(PVec(2, -10, 10), Spawn) @:disp("Start Speed", "Initial speed in the emitter orientation") + public var startSpeed : ParamVec2; + @:param(PVec(2, -10, 10), Spawn) @:disp("Start World Speed", "Initial speed in world space") + public var startWorldSpeed : ParamVec2; + @:param(PVec(2, -10, 10), Particle) @:disp("Speed", "Speed in the emitter orientation, added to the velocity without being accumulated") + public var partSpeed : ParamVec2; + @:param(PVec(2, -10, 10), Particle) @:disp("World Speed", "Speed in world space, added to the velocity without being accumulated") + public var partWorldSpeed : ParamVec2; + @:param(PVec(2, -10, 10), Particle) @:disp("Acceleration", "Acceleration in the emitter orientation, accumulated in the velocity") + public var partAcceleration : ParamVec2; + @:param(PVec(2, -10, 10), Particle) @:disp("World Acceleration", "Acceleration in world space (gravity, wind...), accumulated in the velocity") + public var partWorldAcceleration : ParamVec2; + @:param(PFloat(-360, 360), Particle) @:disp("Orbit Speed", "Rotation speed around the emitter, in degrees per second") + public var partOrbitSpeed : ParamMode; + @:s @:range(0, 2) @:animate @:disp("Orbit Speed Multiplier", "Multiplier of the orbit speed of all the particles, animated over the emitter time") + public var orbitSpeedMultiplier : Float = 1; + + @:group("Limit Velocity") + @:param(PFloat(0, 10), Particle) @:disp("Dampen", "Damping of the accumulated velocity (start speed and accelerations), per second") + public var partDampen : ParamMode; + @:param(PFloat(0, 10), Particle) @:disp("Max Speed", "Maximum speed of the particle") + public var partMaxSpeed : ParamMode; + + @:group("Particle Transform") + @:param(PVec(2, -10, 10), Particle) @:disp("Offset", "Offset of the particle position, without effect on its movement") + public var partOffset : ParamVec2; + @:param(PFloat(0, 360), Particle) @:disp("Rotation", "Rotation of the particle, in degrees") + public var partRotation : ParamMode; + @:param(PFloat(0, 2), Particle, 1.) @:disp("Scale", "Uniform scale of the particle") + public var partScale : ParamMode; + @:s @:range(0, 2) @:animate @:disp("Scale Multiplier", "Multiplier of the scale of all the particles, animated over the emitter time") + public var scaleMultiplier : Float = 1; + @:param(PVec(2, 0, 2), Particle, [1., 1.]) @:disp("Stretch", "Scale of the particle on each of its axes") + public var partStretch : ParamVec2; + + @:group("Rendering") + @:s @:disp("Sprite Type", "Drawing of the particles: a plain color, a texture or a sprite sheet") + public var spriteType : SpriteType = SpriteType.Color; + @:s @:t(PColor) @:disp("Color", "Color of the particles") + public var color : Int = 0xFF000000; + @:s @:disp("Texture", "Texture of the particles") + public var texture : String = ""; + @:s @:disp("Random Color", "Gives each particle a random color instead of the color, taken between two colors or in a gradient") + public var randomColorMode : RandomColorMode = RandomColorMode.None; + @:s @:sameLine @:t(PColor) @:disp("Color 1", "First bound of the random color") + public var randomColor1 : Int = 0xFF000000; + @:s @:sameLine @:t(PColor) @:disp("Color 2", "Second bound of the random color") + public var randomColor2 : Int = 0xFFFFFFFF; + @:s @:sameLine @:disp("Gradient", "Gradient in which the random color is taken") + public var randomGradient : GradientData; + + @:group("Sprite Sheet Animation") + @:s @:t(PFile(["jpg", "png"])) @:disp("Sheet", "Texture containing the frames of the animation") + public var spriteSheet : String; + @:s @:range(0) @:disp("Frames", "Number of frames of the animation") + public var frameCount : Int = 0; + @:s @:range(1) @:disp("Divisions X", "Number of columns of the sheet") + public var frameDivisionX : Int = 1; + @:s @:range(1) @:disp("Divisions Y", "Number of rows of the sheet") + public var frameDivisionY : Int = 1; + @:s @:range(0, 2.0) @:disp("Cycles", "Number of times the animation is played during the life of a particle") + public var animationCycles : Float = 1.0; + @:s @:disp("Loop", "Loops the animation during the life of the particle") + public var animationLoop : Bool = true; + @:s @:disp("Use Source UV", "Keeps the UVs of the sprite inside each frame") + public var animationUseSourceUVs : Bool = true; + @:s @:disp("Blend Frames", "Blends two consecutive frames for a smooth animation") + public var animationBlendBetweenFrames : Bool = true; #if editor var graphics : h2d.Graphics; #end - public function new(parent, shared: ContextShared) { - super(parent, shared); - - props = { }; - for(param in emitterParams) { - if(param.def != null) - EmitterHelper.resetParam(props, param); - } - } - override function save() { var data = super.save(); - data.props = Reflect.copy(props); - for(param in PARAMS) { - var f : Dynamic = Reflect.field(props, param.name); - if(f != null && haxe.Json.stringify(f) != haxe.Json.stringify(param.def)) { - var val : Dynamic = f; - switch(param.t) { - case PEnum(en): - val = Type.enumConstructor(val); - default: - } - Reflect.setField(data.props, param.name, val); - } - else { - Reflect.deleteField(data.props, param.name); - } - } + saveParams(data); + if( randomColorMode != RandomColorMode.Gradient ) + Reflect.deleteField(data, "randomGradient"); + if( !FX.inFXData(this) ) + data.version = FX.VERSION; return data; } override function load( obj : Dynamic ) { + // an emitter out of any FX (in a prefab) is the root of its FX data + if( !FX.inFXData(this) ) + FX.checkVersion(this, obj); super.load(obj); - for(param in emitterParams) { - if(Reflect.hasField(obj.props, param.name)) { - var val : Dynamic = Reflect.field(obj.props, param.name); - switch(param.t) { - case PEnum(en): - #if editor - try { - #end - val = Type.createEnum(en, val); - #if editor - } catch (e) { - val = param.def; - }; - #end - default: - } - Reflect.setField(props, param.name, val); - } - else if(param.def != null) - EmitterHelper.resetParam(props, param); - else if (param.name == "randomGradient") - (props:Dynamic).randomGradient = Gradient.getDefaultGradientData(); - } + loadParams(obj); + if( randomGradient == null && randomColorMode == RandomColorMode.Gradient ) + randomGradient = hrt.impl.Gradient.getDefaultGradientData(); } override function copy(obj:Prefab) { super.copy(obj); - for(param in emitterParams) { - if(Reflect.hasField(obj.props, param.name)) { - var val : Dynamic = Reflect.field(obj.props, param.name); - /*switch(param.t) { - case PEnum(en): - val = Type.createEnum(en, val); - default: - }*/ - Reflect.setField(props, param.name, val); - } - else if(param.def != null) - EmitterHelper.resetParam(props, param); - } + copyParams(cast obj); } override function makeObject(parent2d : h2d.Object) { @@ -637,267 +591,107 @@ class Emitter2D extends Object2D { super.updateInstance(propName); var emitterObj = Std.downcast(local2d, Emitter2DObject); - var randIdx = 0; - - function makeParam(scope: Prefab, name: String): Value { - var getCurve = hrt.prefab.Curve.getCurve.bind(scope); - - function makeCompVal(baseProp: Null, defVal: Float, randProp: Null, pname: String, suffix: String) : Value { - var xVal = Evaluator.vVal(baseProp != null ? baseProp : defVal); - var randCurve = getCurve(pname + suffix + ":rand"); - var randVal : Value = VZero; - if(randCurve != null) - randVal = Evaluator.vMult(VRandom(randIdx++, 1.0, 0.0), Evaluator.vMult(randCurve.makeVal(), VConst(randProp != null ? randProp : 1.0))); - else if(randProp != null && randProp != 0.0) - randVal = VRandom(randIdx++, randProp, 0.0); - - var xCurve = getCurve(pname + suffix); - if (xCurve != null) { - if (xCurve.blendMode == CurveBlendMode.RandomBlend) { - var ca = Std.downcast(xCurve.children[0], Curve); - var cb = Std.downcast(xCurve.children[1], Curve); - return VRandomBetweenCurves(randIdx++, ca, cb); - } - else { - if (pname.indexOf("Rotation") >= 0 || pname.indexOf("Offset") >= 0) - return Evaluator.vAdd(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - else - return Evaluator.vMult(Evaluator.vAdd(xVal, randVal), xCurve.makeVal()); - } - } - else - return Evaluator.vAdd(xVal, randVal); - } - - var baseProp: Dynamic = Reflect.field(props, name); - var randProp: Dynamic = Reflect.field(props, EmitterHelper.randProp(name)); - var param = PARAMS.get(name); - switch(param.t) { - case PVec(_): - inline function makeComp(idx, suffix) { - return makeCompVal( - baseProp != null ? (baseProp[idx] : Float) : null, - param.def != null ? param.def[idx] : 0.0, - randProp != null ? (randProp[idx] : Float) : null, - param.name, suffix); - } - var v : Value = VVector( - makeComp(0, ":x"), - makeComp(1, ":y"), - makeComp(2, ":z")); - if(v.match(VVector(VZero, VZero, VZero))) - v = VZero; - else if(v.match(VVector(VConst(1.0), VConst(1.0), VConst(1.0)))) - v = VConst(1.0); - return v; - - default: - return makeCompVal(baseProp, param.def != null ? param.def : 0.0, randProp, param.name, ""); - } - } - - var d = new InstanceDef(); - d.localSpeed = makeParam(this, "instSpeed"); - d.worldSpeed = makeParam(this, "instWorldSpeed"); - d.orbitSpeed = makeParam(this, "instOrbitSpeed"); - d.orbitSpeedOverTime = makeParam(this, "instOrbitSpeedOverTime"); - d.acceleration = makeParam(this, "instAcceleration"); - d.worldAcceleration = makeParam(this, "instWorldAcceleration"); - d.localOffset = makeParam(this, "instOffset"); - d.scale = makeParam(this, "instScale"); - d.scaleOverTime = makeParam(this, "instScaleOverTime"); - d.dampen = makeParam(this, "instDampen"); - d.maxVelocity = makeParam(this, "instMaxVelocity"); - d.stretch = makeParam(this, "instStretch"); - d.rotation = makeParam(this, "instRotation"); - emitterObj.instDef = d; + emitterObj.evaluator = makeEvaluator(); emitterObj.blendMode = this.blendMode; emitterObj.batch.blendMode = this.blendMode; - emitterObj.spriteType = EmitterHelper.getParamVal(PARAMS, props, "spriteType"); - emitterObj.texture = EmitterHelper.getParamVal(PARAMS, props, "texture"); - emitterObj.color = EmitterHelper.getParamVal(PARAMS, props, "color"); - emitterObj.seed = EmitterHelper.getParamVal(PARAMS, props, "seed"); - emitterObj.lifeTime = EmitterHelper.getParamVal(PARAMS, props, "lifeTime"); - emitterObj.lifeTimeRand = EmitterHelper.getParamVal(PARAMS, props, "lifeTimeRand"); - emitterObj.speedFactor = EmitterHelper.getParamVal(PARAMS, props, "speedFactor"); - emitterObj.warmUpTime = EmitterHelper.getParamVal(PARAMS, props, "warmUpTime"); - emitterObj.delay = EmitterHelper.getParamVal(PARAMS, props, "delay"); - emitterObj.emitType = EmitterHelper.getParamVal(PARAMS, props, "emitType"); - emitterObj.burstCount = EmitterHelper.getParamVal(PARAMS, props, "burstCount"); - emitterObj.burstDelay = EmitterHelper.getParamVal(PARAMS, props, "burstDelay"); - emitterObj.burstParticleCount = makeParam(this, "burstParticleCount"); - emitterObj.emitDuration = EmitterHelper.getParamVal(PARAMS, props, "emitDuration"); - emitterObj.emitOrientation = EmitterHelper.getParamVal(PARAMS, props, "emitOrientation"); - emitterObj.maxCount = EmitterHelper.getParamVal(PARAMS, props, "maxCount"); - emitterObj.emitRate = makeParam(this, "emitRate"); - emitterObj.emitRateMin = makeParam(this, "emitRateMin"); - emitterObj.emitRateMax = makeParam(this, "emitRateMax"); - emitterObj.emitRateChangeDelay = EmitterHelper.getParamVal(PARAMS, props, "emitRateChangeDelay"); - emitterObj.emitShape = EmitterHelper.getParamVal(PARAMS, props, "emitShape"); - emitterObj.emitRadius = makeParam(this, "emitRadius"); - emitterObj.emitAngle1 = makeParam(this, "emitAngle1"); - emitterObj.emitAngle2 = makeParam(this, "emitAngle2"); - emitterObj.emitWidth = makeParam(this, "emitWidth"); - emitterObj.emitHeight = makeParam(this, "emitHeight"); - emitterObj.emitSurface = EmitterHelper.getParamVal(PARAMS, props, "emitSurface"); - emitterObj.spriteSheet = EmitterHelper.getParamVal(PARAMS, props, "spriteSheet"); - emitterObj.frameCount = EmitterHelper.getParamVal(PARAMS, props, "frameCount"); - emitterObj.frameDivisionX = EmitterHelper.getParamVal(PARAMS, props, "frameDivisionX"); - emitterObj.frameDivisionY = EmitterHelper.getParamVal(PARAMS, props, "frameDivisionY"); - emitterObj.animationSpeed = EmitterHelper.getParamVal(PARAMS, props, "animationSpeed"); - emitterObj.animationLoop = EmitterHelper.getParamVal(PARAMS, props, "animationLoop"); - emitterObj.animationUseSourceUVs = EmitterHelper.getParamVal(PARAMS, props, "animationUseSourceUVs"); - emitterObj.animationBlendBetweenFrames = EmitterHelper.getParamVal(PARAMS, props, "animationBlendBetweenFrames"); - emitterObj.useRandomColor = EmitterHelper.getParamVal(PARAMS, props, "useRandomColor"); - emitterObj.useRandomGradient = EmitterHelper.getParamVal(PARAMS, props, "useRandomGradient"); - emitterObj.randomColor1 = EmitterHelper.getParamVal(PARAMS, props, "randomColor1"); - emitterObj.randomColor2 = EmitterHelper.getParamVal(PARAMS, props, "randomColor2"); - emitterObj.randomGradient = EmitterHelper.getParamVal(PARAMS, props, "randomGradient"); - emitterObj.startSpeed = makeParam(this, "instStartSpeed"); - emitterObj.startWorldSpeed = makeParam(this, "instStartWorldSpeed"); - - emitterObj.init(randIdx, this); + + emitterObj.init(this); #if editor - if(propName == null || ["emitShape", "emitRadius", "emitAngle1", "emitAngle2", "emitWidth", "emitHeight"].indexOf(propName) >= 0) + if(propName == null || ["emitShape", "emitRadius", "emitStartAngle", "emitEndAngle", "emitWidth", "emitHeight"].indexOf(propName) >= 0) updateEmitShape(emitterObj); #end } - #if editor - override function edit( ctx : hide.prefab.EditContext ) { - super.edit(ctx); - - function refresh() { - ctx.rebuildProperties(); - } - - function onChange(?pname: String) { - if (pname == "useRandomGradient") { - if ((props:Dynamic).useRandomGradient == true) { - (props:Dynamic).randomGradient = Gradient.getDefaultGradientData(); - } else { - Reflect.deleteField(props, "randomGradient"); - } - refresh(); - } - - if (pname == "spriteType") { - if ((props:Dynamic).spriteType != SpriteType.SpriteSheet) - (props:Dynamic).spriteSheet = null; - refresh(); - } - - ctx.onChange(this, pname); - - if (pname == "warmUpTime") { - var props : Dynamic = cast props; - if (props.warmUpTime < 0) { - props.warmUpTime = 0; - hide.Ide.inst.quickError("Warm up time can no longer be negative. Use the Delay property instead"); - refresh(); - } - } - - if(["emitShape", - "useCollision", - "emitType", - "useRandomColor" - ].indexOf(pname) >= 0) { - refresh(); - } - } - - var params = emitterParams.copy(); - inline function removeParam(pname: String) { - params.remove(params.find(p -> p.name == pname)); - } + #if (editor || editor_hl) + function getHiddenFields() : Array { + var hidden = []; + inline function hide(name: String) + hidden.push(name); - var emitShape : Emit2DShape = EmitterHelper.getParamVal(PARAMS, props, "emitShape"); if(!emitShape.match(Circle)) { - removeParam("emitRadius"); - removeParam("emitAngle1"); - removeParam("emitAngle2"); + hide("emitRadius"); + hide("emitStartAngle"); + hide("emitEndAngle"); } if(!emitShape.match(Rectangle)) { - removeParam("emitWidth"); - removeParam("emitHeight"); + hide("emitWidth"); + hide("emitHeight"); } - var spriteType = EmitterHelper.getParamVal(PARAMS, props, "spriteType"); if (spriteType != SpriteType.Color) { - removeParam("color"); - removeParam("useRandomColor"); + hide("color"); + hide("randomColorMode"); } if (spriteType != SpriteType.Texture) { - removeParam("texture"); + hide("texture"); } if (spriteType != SpriteType.SpriteSheet) { - removeParam("spriteSheet"); - removeParam("frameCount"); - removeParam("frameDivisionX"); - removeParam("frameDivisionY"); - removeParam("animationSpeed"); - removeParam("animationLoop"); - removeParam("animationUseSourceUVs"); - removeParam("animationBlendBetweenFrames"); + hide("spriteSheet"); + hide("frameCount"); + hide("frameDivisionX"); + hide("frameDivisionY"); + hide("animationCycles"); + hide("animationLoop"); + hide("animationUseSourceUVs"); + hide("animationBlendBetweenFrames"); } - if(!EmitterHelper.getParamVal(PARAMS, props, "useRandomColor")) { - removeParam("useRandomGradient"); - removeParam("randomColor1"); - removeParam("randomColor2"); - removeParam("randomGradient"); - } - else { - if (EmitterHelper.getParamVal(PARAMS, props, "useRandomGradient")){ - removeParam("randomColor1"); - removeParam("randomColor2"); - } else { - removeParam("randomGradient"); - } + if (spriteType != SpriteType.Color || randomColorMode != RandomColorMode.Colors) { + hide("randomColor1"); + hide("randomColor2"); } + if (spriteType != SpriteType.Color || randomColorMode != RandomColorMode.Gradient) + hide("randomGradient"); - var emitType : EmitType = EmitterHelper.getParamVal(PARAMS, props, "emitType"); switch (emitType) { case Infinity: - removeParam("burstCount"); - removeParam("burstDelay"); - removeParam("burstParticleCount"); - removeParam("emitDuration"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); - case InfinityRandom: - removeParam("emitRate"); - removeParam("burstCount"); - removeParam("burstDelay"); - removeParam("burstParticleCount"); - removeParam("emitDuration"); - case BurstDuration: - removeParam("emitRate"); - removeParam("burstCount"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); - case Burst: - removeParam("emitDuration"); - removeParam("emitRate"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + hide("burstCount"); + hide("burstInterval"); + hide("burstParticleCount"); + hide("emitDuration"); case Duration: - removeParam("burstCount"); - removeParam("burstDelay"); - removeParam("burstParticleCount"); - removeParam("emitRateMin"); - removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + hide("burstCount"); + hide("burstInterval"); + hide("burstParticleCount"); + case Burst: + hide("emitDuration"); + hide("emitRate"); + case BurstDuration: + hide("burstCount"); + hide("emitRate"); + } + // read only by a random rate, which bursts do not use + var randomRate = switch (getMode("emitRate", 0)) { + case Range(_), RandomCurve(_), CurveRange: true; + case null, _: false; + } + if (!randomRate || emitType == Burst || emitType == BurstDuration) + hide("emitRateChangeTime"); + + return hidden; + } + + static var REFRESH_PARAMS = ["emitShape", "emitType", "spriteType", "randomColorMode"]; + #end + + override function edit2( ctx : hrt.prefab.EditContext2 ) { + super.edit2(ctx); + + function onChange(pname: String, isTemp: Bool) { + if (pname == "randomColorMode") + randomGradient = randomColorMode == RandomColorMode.Gradient ? hrt.impl.Gradient.getDefaultGradientData() : null; + if (pname == "spriteType" && spriteType != SpriteType.SpriteSheet) + spriteSheet = null; + ctx.rebuildPrefab(this); + if (!isTemp && REFRESH_PARAMS.contains(pname)) + ctx.rebuildInspector(); } - EmitterHelper.generateEdit(params, instanceParams, props, ctx.properties, onChange, refresh); + EmitterHelper.edit(ctx, this, getHiddenFields(), onChange); } + #if editor override function getHideProps() : hide.prefab.HideProps { return { icon : "asterisk", @@ -915,14 +709,14 @@ class Emitter2D extends Object2D { graphics = new h2d.Graphics(e); graphics.lineStyle(1, 0xFFFFFF, 1); - switch(e.emitShape) { + switch(emitShape) { case Circle: - var radA1 = e.evaluator.getFloat(0, e.emitAngle1, 0) * hxd.Math.PI / 180; - var radA2 = e.evaluator.getFloat(0, e.emitAngle2, 0) * hxd.Math.PI / 180; - graphics.drawPie(0, 0, e.evaluator.getFloat(0, e.emitRadius, 0), radA1, radA2 - radA1); + var radA1 = e.evaluator.getFloat(e.evaluator.emitStartAngle, 0) * hxd.Math.PI / 180; + var radA2 = e.evaluator.getFloat(e.evaluator.emitEndAngle, 0) * hxd.Math.PI / 180; + graphics.drawPie(0, 0, e.evaluator.getFloat(e.evaluator.emitRadius, 0), radA1, radA2 - radA1); case Rectangle: - var width = e.evaluator.getFloat(0, e.emitWidth, 0); - var height = e.evaluator.getFloat(0, e.emitHeight, 0); + var width = e.evaluator.getFloat(e.evaluator.emitWidth, 0); + var height = e.evaluator.getFloat(e.evaluator.emitHeight, 0); graphics.drawRect(-width / 2, -height / 2, width, height); } } diff --git a/hrt/prefab/fx/EmitterHelper.hx b/hrt/prefab/fx/EmitterHelper.hx index 2d9498e0f..593a6e8a1 100644 --- a/hrt/prefab/fx/EmitterHelper.hx +++ b/hrt/prefab/fx/EmitterHelper.hx @@ -1,68 +1,89 @@ package hrt.prefab.fx; -import hrt.prefab.Curve; +import hrt.prefab.fx.EmitterParam; -typedef ParamDef = { +typedef PropertyDef = { > hrt.prefab.Props.PropDef, ?animate: Bool, - ?instance: Bool, - ?groupName: String + ?groupName: String, + ?sameLine: Bool, + ?line: String } class EmitterHelper { - public static function makeColor(scope: Prefab, name: String) { - var curves = hrt.prefab.Curve.getCurves(scope, name); - if(curves == null || curves.length == 0) - return null; - return hrt.prefab.Curve.getColorValue(curves); + + // the mode inspector edits the params of an emitter or of a shader under one (in its `particleParams`) + static function getMode(p: Prefab, name: String, comp: Int) : ParamMode { + if (p is IEmitter) + return (cast p : IEmitter).getMode(name, comp); + var shader : hrt.prefab.Shader = cast p; + return shader.particleParams?.get(EmitterParam.curveName(name, shaderParamSize(shader, name), comp)); } - public static function randProp(name: String) { - return name + "_rand"; + static function setMode(p: Prefab, name: String, comp: Int, m: ParamMode) { + if (p is IEmitter) { + (cast p : IEmitter).setMode(name, comp, m); + return; + } + var shader : hrt.prefab.Shader = cast p; + var size = shaderParamSize(shader, name); + var key = EmitterParam.curveName(name, size, comp); + shader.particleParams?.remove(key); + switch (m) { + case Constant(k): + if (size == 1) + Reflect.setField(shader.props, name, k); + else { + var prop : Array = Reflect.field(shader.props, name); + if (!Std.isOfType(prop, Array)) { + prop = [for (j in 0...size) EmitterParam.baseComponent(prop, size, j)]; + Reflect.setField(shader.props, name, prop); + } + prop[comp] = k; + } + case null: + default: + if (shader.particleParams == null) + shader.particleParams = []; + shader.particleParams.set(key, m); + } + if (shader.particleParams != null && !shader.particleParams.iterator().hasNext()) + shader.particleParams = null; } - public static function resetParam(props : Dynamic, param: ParamDef) { - if(param.def is Array) - Reflect.setProperty(props, param.name, cast(param.def, Array).copy()); - else - Reflect.setProperty(props, param.name, param.def); + static function shaderParamSize(shader: hrt.prefab.Shader, name: String) { + return EmitterParam.getParamSize(Lambda.find(shader.getShaderDefinition().data.vars, v -> v.name == name)); } - public static function toArray( v : Dynamic ) : Array { - if( v == null ) - return null; - var a : Array = v; - return [for( x in a ) (x : Float)]; + // a random in [0, 1) for the step `i` of an emitter of seed `seed`: the same whatever the ticks (a loop or a seek included) + static inline function stepRandom( i : Int, seed : Int ) : Float { + return (hxd.Rand.inlineHash(i, seed) & 0xFFFFFF) / 0x1000000; } - public static function sameArray( a : Array, b : Array ) { - if( a == null || b == null ) - return a == b; - if( a.length != b.length ) - return false; - for( i in 0...a.length ) - if( a[i] != b[i] ) - return false; - return true; + // the emission rate at `time`: its random goes to a new value every `changeTime` + public static function getRate( ev : Evaluator, rate : Value, time : Float, changeTime : Float, seed : Int ) : Float { + var step = time / Math.max(changeTime, 0.01), i = Math.floor(step); + return ev.getAtRandom(rate, time, hxd.Math.lerp(stepRandom(i, seed), stepRandom(i + 1, seed), step - i)); } - public static function getParamVal(params: Map, props: Dynamic, name: String, rand: Bool=false) : Dynamic { - var param = params.get(name); - if(param == null) - return Reflect.field(props, name); - var isVector = switch(param.t) { - case PVec(_): true; - default: false; + // the particles emitted from 0 to `time`, approximated for a random rate by the middle of its range + public static function getRateSum( ev : Evaluator, rate : Value, time : Float ) : Float { + return switch( rate ) { + case VRandom(_, min, max): (min + max) * 0.5 * time; + case VRandomCurve(_, c, _, _): c.getSum(time); + case VRandomParamCurve(_, pc, _, _): ev.getSum(VParamCurve(pc), time); + case VCurveRange(_, a, b): (a.getSum(time) + b.getSum(time)) * 0.5; + default: ev.getSum(rate, time); } - var val : Dynamic = rand ? (isVector ? [0.,0.,0.,0.] : 0.) : param.def; - if(rand) - name = EmitterHelper.randProp(name); - if(props != null && Reflect.hasField(props, name)) { - val = Reflect.field(props, name); + } + + // the particles of the burst number `burst`, a range drawn for each burst (both bounds included) + public static function getBurstCount( ev : Evaluator, count : Value, time : Float, burst : Int, seed : Int ) : Int { + var u = stepRandom(burst, seed + 1); + return switch( count ) { + case VRandom(_, min, max): Std.int(hxd.Math.lerp(Math.min(min, max), Math.max(min, max) + 1, u)); + default: Std.int(ev.getAtRandom(count, time, u)); } - if(isVector) - return h3d.Vector.fromArray(val); - return val; } public static inline function rotateZ( m : h3d.Matrix, angle : Float ) { @@ -149,130 +170,463 @@ class EmitterHelper { return head; } - #if editor - public static function generateEdit(params : Array, instanceParams : Array, props : Dynamic, properties : hide.comp.PropsEditor, onChange : (?pname : String) -> Void, refresh : Void -> Void, typed = false) { - inline function isSet(name: String) return typed ? Reflect.getProperty(props, name) != null : Reflect.hasField(props, name); - inline function unset(name: String) if( typed ) Reflect.setProperty(props, name, null) else Reflect.deleteField(props, name); - // Emitter - { - // Sort by groupName - var groupNames : Array = []; - for( p in params ) { - if( p.groupName == null && groupNames.indexOf("Emitter") == -1 ) - groupNames.push("Emitter"); - else if( p.groupName != null && groupNames.indexOf(p.groupName) == -1 ) - groupNames.push(p.groupName); - } + #if domkit - for( gn in groupNames ) { - var params = params.filter( p -> p.groupName == (gn == "Emitter" ? null : gn) ); - var group = new hide.Element('
'); - group.append(hide.comp.PropsEditor.makePropsList(params)); - properties.add(group, props, onChange); + // when a param is read, and the time of its curves + static function clockDesc(kind: ParamKind) { + return switch (kind) { + case Emission: "Read at each emission step, curves over the emitter time"; + case Spawn: "Read once at the spawn of the particle, curves over the emitter time"; + case Particle: "Read over the life of the particle, curves over its life (0 to 1)"; + } + } + + // Build inspector for an emitter parameter + static function editParam(ctx: EditContext2, prefab: Prefab, def: ParamDef, allowed: Array, parent: hide.kit.Element) { + var size = def.def.length; + var label = def.disp ?? hide.kit.Macros.camelToSpaceCase(def.name); + var range = switch (def.t) { + case PFloat(min, max), PVec(_, min, max): { min: min, max: max }; + default: { min: null, max: null }; + } + // the clock of the param: kit-param-emission, kit-param-spawn, kit-param-particle + var kindClass = "kit-param-" + def.kind.getName().toLowerCase(); + var tooltip = (def.desc == null ? "" : def.desc + "\n") + clockDesc(def.kind); + var comps = [for (i in 0...size) i]; + var value = [for (i in comps) shownMode(prefab, def, i)]; + + function applyMode(comps: Array, mode: Null) { + var before = getModeCurves(prefab); + ctx.recordPrefabUndoWithChildren(prefab, () -> { + for (i in comps) + setParamMode(prefab, def, i, mode == null ? null : convert(prefab, def, i, mode)); + removeUnusedModeCurves(prefab, before); + }); + ctx.rebuildInspector(); + } + + function contextMenu(i: Int) : Array { + var m = shownMode(prefab, def, i); + var kind : ModeKind = m == null ? Constant : cast m.getName(); + var items : Array = []; + if (size > 1) + items.push({ isSeparator: true, label: label + " " + compName(i) }); + if (allowed.length > 1) { + for (mode in allowed) + items.push({ label: hide.kit.Macros.camelToSpaceCase(mode), radio: () -> kind == mode, click: () -> applyMode([i], mode) }); + items.push({ isSeparator: true }); } + items.push({ label: size == 1 ? "Remove" : "Remove " + compName(i), click: () -> applyMode([i], null), enabled: isSet(prefab, def, i) }); + return items; + } + + function setupVectorHeader(line: hide.kit.Line) { + line.label = label; + line.unset = !isAnySet(prefab, def); + line.menuItems = () -> [ + { isSeparator: true, label: label }, + { label: "Remove all", click: () -> applyMode(comps, null), enabled: isAnySet(prefab, def) }, + ]; } - // Instances - { - var groups = new Map>(); - for(p in instanceParams) { - var groupName = p.groupName != null ? p.groupName : "Particles"; + var line = null; + var isCompact = size > 1 && Lambda.foreach(value, m -> m == null || m.match(Constant(_))); + if (isCompact) { + line = new hide.kit.Line(parent, def.name); + line.classList.push(kindClass); + line.stealLabel = false; + line.tooltip = tooltip; + setupVectorHeader(line); + } - if (!groups.exists(groupName)) - groups.set(groupName, []); - groups[groupName].push(p); + for (i in comps) { + var menuOwners : Array = []; + if (!isCompact) { + line = new hide.kit.Line(parent, def.name + (size == 1 ? "" : compName(i))); + line.classList.push(kindClass); + line.stealLabel = false; + line.tooltip = tooltip; + if (size == 1) { + line.label = label; + line.unset = !isSet(prefab, def, i); + menuOwners.push(line); + } else { + if (i == 0) + setupVectorHeader(line); + var name = new hide.kit.Text(line, "comp", compName(i)); + name.classList.push("kit-param-comp"); + name.unset = !isSet(prefab, def, i); + menuOwners.push(name); + } + } + var wasSet = isSet(prefab, def, i); + function slider(sid: String, label: String, value: Float) { + var s = new hide.kit.Slider(line, sid); + if (size > 1) + menuOwners.push(s); + s.label = label; + s.value = value; + s.min = range.min; + s.max = range.max; + s.defaultValue = def.def[i]; + s.showRange = range.min != null && range.max != null; + s.onValueChange = (temp) -> { + ctx.rebuildPrefab(prefab); + if (!temp && isSet(prefab, def, i) != wasSet) + ctx.rebuildInspector(); + } + return s; } - for (groupName => params in groups) - { - var instGroup = new hide.Element('
'); - var dl = new hide.Element('
').appendTo(instGroup); - - for (p in params) { - var dt = new hide.Element('
${p.disp != null ? p.disp : p.name}
').appendTo(dl); - var dd = new hide.Element('
').appendTo(dl); - - function addUndo(pname: String) { - properties.undo.change(Field(props, pname, Reflect.getProperty(props, pname)), function() { - if(Reflect.getProperty(props, pname) == null) - unset(pname); - refresh(); - }); + function curveEditor(?spread: hide.kit.Curve.TrackSpread, ?onSpreadChange: hide.kit.Curve.TrackSpread -> Void) { + var g = new hide.kit.Curve(line, "curve", (size == 1 ? label : label + " " + compName(i)) + " - " + prefab.name); + // over the emitter time, else over the particle life + if (def.kind != Particle) + g.maxTime = Math.POSITIVE_INFINITY; + g.onValueChange = (_) -> ctx.rebuildPrefab(prefab); + if (onSpreadChange != null) + g.onSpreadChange = onSpreadChange; + if (size > 1) + menuOwners.push(g); + var edited = getEditableCurve(prefab, EmitterParam.curveKey(def, i)); + if (edited == null) + g.addTrack(null, spread); + else + for (d in edited.datas) + g.addTrack(d, spread); + if (g.tracks.length == 2) { + g.addBand(g.tracks[0], g.tracks[1]); + g.tracks[0].name = "Min"; + g.tracks[1].name = "Max"; + } + } + + switch (value[i]) { + case null, Constant(_): + var v = switch (value[i]) { + case Constant(c): c; + case null, _: def.def[i]; + } + + var s = slider("value" + (isCompact ? compName(i) : ""), isCompact ? compName(i) : "", v); + s.unset = !wasSet; + @:privateAccess s.onFieldChange = (_) -> { + setParamMode(prefab, def, i, Constant(s.value)); + s.unset = s.value == def.def[i]; + } + + case Range(min, max): + var minSlider = slider("min", "Min", min); + var maxSlider = slider("max", "Max", max); + function bound() { + minSlider.max = range.max == null ? maxSlider.value : Math.min(range.max, maxSlider.value); + maxSlider.min = range.min == null ? minSlider.value : Math.max(range.min, minSlider.value); } - if(isSet(p.name)) { - hide.comp.PropsEditor.makePropEl(p, dd); - dt.contextmenu(function(e) { - e.preventDefault(); - hide.comp.ContextMenu.createFromEvent(cast e, [ - { label : "Reset", click : function() { - addUndo(p.name); - EmitterHelper.resetParam(props, p); - onChange(); - refresh(); - } }, - { label : "Remove", click : function() { - addUndo(p.name); - unset(p.name); - onChange(); - refresh(); - } }, - ]); - return false; - }); + // typing sends the unchanged value: no edit, that would refresh the field being typed + function apply() { + if (minSlider.value == min && maxSlider.value == max) + return; + min = minSlider.value; + max = maxSlider.value; + bound(); + setParamMode(prefab, def, i, Range(min, max)); } - else { - var btn = new hide.Element('').appendTo(dd); - btn.click(function(e) { - addUndo(p.name); - EmitterHelper.resetParam(props, p); - refresh(); - }); + + @:privateAccess minSlider.onFieldChange = (_) -> { + if (minSlider.value > maxSlider.value) + minSlider.value = maxSlider.value; + apply(); } - var dt = new hide.Element('
~
').appendTo(dl); - var dd = new hide.Element('
').appendTo(dl); - var randDef : Dynamic = switch(p.t) { - case PVec(n): [for(i in 0...n) 0.0]; - case PFloat(_): 0.0; - default: 0; - }; - if(isSet(EmitterHelper.randProp(p.name))) { - hide.comp.PropsEditor.makePropEl({ - name: EmitterHelper.randProp(p.name), - t: p.t, - def: randDef}, dd); - dt.contextmenu(function(e) { - e.preventDefault(); - hide.comp.ContextMenu.createFromEvent(cast e, [ - { label : "Reset", click : function() { - addUndo(EmitterHelper.randProp(p.name)); - Reflect.setProperty(props, EmitterHelper.randProp(p.name), randDef); - onChange(); - refresh(); - } }, - { label : "Remove", click : function() { - addUndo(EmitterHelper.randProp(p.name)); - unset(EmitterHelper.randProp(p.name)); - onChange(); - refresh(); - } }, - ]); - return false; - }); + @:privateAccess maxSlider.onFieldChange = (_) -> { + if (maxSlider.value < minSlider.value) + maxSlider.value = minSlider.value; + apply(); } - else { - var btn = new hide.Element('').appendTo(dd); - btn.click(function(e) { - addUndo(EmitterHelper.randProp(p.name)); - Reflect.setProperty(props, EmitterHelper.randProp(p.name), randDef); - refresh(); - }); + bound(); + case Curve, CurveRange: + curveEditor(); + case RandomCurve(spreadMin, spreadMax): + curveEditor({ min: spreadMin, max: spreadMax }, sp -> setParamMode(prefab, def, i, RandomCurve(sp.min, sp.max))); + } + for (o in menuOwners) + o.menuItems = () -> contextMenu(i); + } + } + + // Build inspector for an emitter + public static function edit(ctx: EditContext2, emitter: Prefab, hidden: Array, changed: (name: String, temp: Bool) -> Void) { + + var categories = new Map(); + function getCategory(name: String) { + var cat = categories.get(name); + if (cat == null) { + cat = new hide.kit.Category(ctx.root, name, name); + categories.set(name, cat); + } + return cat; + } + + function isShown(f: EmitterField) { + return switch (f) { + case Property(p): !hidden.contains(p.name); + case Param(p): !hidden.contains(p.name); + } + } + var fields = [for (f in (cast emitter : IEmitter).getFields()) if (isShown(f)) f]; + // the line of a property followed by `sameLine` ones: named by its `line`, the widgets keep their labels, + // else it takes the label of the first one and the others show none + var line : hide.kit.Line = null; + for (i => f in fields) { + switch (f) { + case Property(p) if (p.sameLine == true && line != null): + var widget = editProperty(line, emitter, p, changed); + if (widget != null && line.label == null) + widget.label = null; + case Property(p): + var parent : hide.kit.Element = getCategory(p.groupName ?? "Emitter"); + line = switch (fields[i + 1]) { + case Property(next) if (next.sameLine == true): + var l = new hide.kit.Line(parent, p.name + "Line"); + if (p.line != null) { + l.label = p.line; + l.stealLabel = false; + } else + l.tooltip = p.desc; + l; + case null, _: null; } + var widget = editProperty(line ?? parent, emitter, p, changed); + if (line != null && line.label == null && widget != null) + widget.classList.push("kit-line-first"); + case Param(p): + line = null; + editParam(ctx, emitter, p, [Constant, Range, Curve, RandomCurve, CurveRange], getCategory(p.groupName)); + } + } + } + + static function editProperty(parent: hide.kit.Element, emitter: Prefab, p: PropertyDef, changed: (name: String, temp: Bool) -> Void) : hide.kit.Widget { + switch (p.t) { + case PVec(n, min, max) if (p.name.toLowerCase().indexOf("color") < 0): + var line = new hide.kit.Line(parent, p.name); + line.label = p.disp ?? hide.kit.Macros.camelToSpaceCase(p.name); + line.tooltip = p.desc; + var vec : Array = Reflect.field(emitter, p.name); + for (i in 0...n) { + var s = new hide.kit.Slider(line, p.name + i); + s.label = compName(i); + s.value = vec[i]; + s.min = min; + s.max = max; + s.showRange = min != null && max != null; + @:privateAccess s.onFieldChange = (_) -> vec[i] = s.value; + s.onValueChange = (temp) -> changed(p.name, temp); } + return null; + default: + var widget = parent.buildProp(p, emitter); + if (widget == null) + return null; + if (!p.animate) + widget.anim = null; + widget.onValueChange = (temp) -> changed(p.name, temp); + return widget; + } + } - properties.add(instGroup, props, onChange); + + static function compName(i: Int) { + return EmitterParam.COMPONENTS[i].toUpperCase(); + } + + static function isDefault(def: ParamDef, i: Int, m: ParamMode) { + return switch (m) { + case null: true; + case Constant(v): v == def.def[i]; + default: false; + } + } + + // a curve mode whose curve was deleted shows the default value + static function shownMode(prefab: Prefab, def: ParamDef, i: Int) { + return EmitterParam.saved(prefab, def, i, getMode(prefab, def.name, i)); + } + + static function isSet(prefab: Prefab, def: ParamDef, i: Int) { + return !isDefault(def, i, shownMode(prefab, def, i)); + } + + static function isAnySet(prefab: Prefab, def: ParamDef) { + return Lambda.exists([for (i in 0...def.def.length) i], i -> isSet(prefab, def, i)); + } + + static function setParamMode(prefab: Prefab, def: ParamDef, i: Int, m: ParamMode) { + setMode(prefab, def.name, i, isDefault(def, i, m) ? null : m); + } + + static function convert(prefab: Prefab, def: ParamDef, i: Int, mode: ModeKind) : ParamMode { + var base = def.def[i]; + var key = EmitterParam.curveKey(def, i); + var cur = getEditableCurve(prefab, key); + function at(k: Int, t: Float) { return cur?.datas[k]?.getVal(t) ?? base; } + // the keys of the current curve remapped from 0..1 to `from`..`to`, or a line + function data(src: Bool, from: Float, to: Float) { return newModeData(src ? cur : null, from, to); } + // the spread of a RandomCurve, `spread + slope * value` + function spread(spreadMin: Float, spreadMax: Float) { + var c = cur?.datas[0]; + return EmitterParam.spreadLine(c == null ? { min: base, max: base } : c.getValueRange(), spreadMin, spreadMax); + } + function curve(datas: Array) { setModeCurve(prefab, key, datas); } + var m = shownMode(prefab, def, i) ?? Constant(base); + var f0 = base, f1 = base, t0 = base, t1 = base; + switch (m) { + case Constant(v): f0 = f1 = t0 = t1 = v; + case Range(min, max): f0 = t0 = min; f1 = t1 = max; + case Curve: f0 = f1 = at(0, 0); t0 = t1 = at(0, 1); + case RandomCurve(spreadMin, spreadMax): + var l = spread(spreadMin, spreadMax), v0 = at(0, 0), v1 = at(0, 1); + var a0 = l.spread + l.slope * v0, a1 = l.spread + l.slope * v1; + f0 = v0 - a0; f1 = v0 + a0; t0 = v1 - a1; t1 = v1 + a1; + case CurveRange: f0 = at(0, 0); t0 = at(0, 1); f1 = at(1, 0); t1 = at(1, 1); + } + var fm = (f0 + f1) * 0.5, tm = (t0 + t1) * 0.5; + return switch [mode, m] { + case [Constant, _]: Constant(tm); + case [Range, _]: Range(t0, t1); + case [Curve, Curve]: m; + case [Curve, CurveRange]: curve([data(true, 0, 1)]); Curve; + case [Curve, RandomCurve(_)]: Curve; + case [Curve, _]: curve([data(false, fm, tm)]); Curve; + case [RandomCurve, RandomCurve(_)]: m; + case [RandomCurve, Curve]: RandomCurve(0, 0); + case [RandomCurve, CurveRange]: curve([data(true, 0, 1)]); RandomCurve(0, 0); + // a line from fm to tm: `spreadMin` is the spread at its lowest end + case [RandomCurve, _]: + curve([data(false, fm, tm)]); + fm <= tm ? RandomCurve((f1 - f0) * 0.5, (t1 - t0) * 0.5) : RandomCurve((t1 - t0) * 0.5, (f1 - f0) * 0.5); + case [CurveRange, CurveRange]: m; + case [CurveRange, Curve]: curve([data(true, 0, 1), data(false, f1, t1)]); CurveRange; + // v -+ (spread + slope * v): the values of the curve remapped + case [CurveRange, RandomCurve(spreadMin, spreadMax)]: + var l = spread(spreadMin, spreadMax); + curve([data(true, -l.spread, 1 - l.slope - l.spread), data(true, l.spread, 1 + l.slope + l.spread)]); + CurveRange; + case [CurveRange, _]: curve([data(false, f0, t0), data(false, f1, t1)]); CurveRange; + } + } + + static function setModeCurve(owner: Prefab, name: String, datas: Array) { + var c = hrt.prefab.fx.Curve.getCurve(owner, name); + if (c == null) { + c = new hrt.prefab.fx.Curve(owner, owner.shared); + c.name = name; + } + c.datas = datas; + c.blendMode = datas.length == 2 ? hrt.prefab.fx.Curve.CurveBlendMode.Range : None; + c.param = null; + c.refPath = null; + } + + static function newModeData(src: Curve, from: Float, to: Float) : hrt.impl.Curve { + var d = new hrt.impl.Curve(); + if (src == null) { + d.addKey(0, from, Linear); + d.addKey(1, to, Linear); + return d; + } + @:privateAccess d.copyFromDynamic(hrt.prefab.Diff.deepCopy(src.data.copyToDynamic(null))); + for (k in d.keys) { + k.value = hxd.Math.lerp(from, to, k.value); + for (h in [k.prevHandle, k.nextHandle]) + if (h != null) + h.dv *= to - from; + } + return d; + } + + static function getModeCurves(owner: Prefab) : Array { + var names = []; + var shader = Std.downcast(owner, hrt.prefab.Shader); + if (shader != null) { + if (shader.particleParams != null) + names = [for (k => m in shader.particleParams) if (EmitterParam.isCurveMode(m)) k]; + } + else { + var emitter : IEmitter = cast owner; + for (f in emitter.getFields()) + switch (f) { + case Param(p): + for (i in 0...p.def.length) + if (EmitterParam.isCurveMode(emitter.getMode(p.name, i))) + names.push(EmitterParam.curveKey(p, i)); + default: + } + } + var curves = []; + for (n in names) { + var c = hrt.prefab.fx.Curve.getCurve(owner, n); + if (c != null) + curves.push(c); + } + return curves; + } + + static function removeUnusedModeCurves(owner: Prefab, before: Array) { + var after = getModeCurves(owner); + for (c in before) + if (!after.contains(c)) + c.remove(); + } + + static function getEditableCurve(owner: Prefab, name: String) : Curve { + var c = hrt.prefab.fx.Curve.getCurve(owner, name); + return c != null && (c.isPlain() || c.blendMode == hrt.prefab.fx.Curve.CurveBlendMode.Range) ? c : null; + } + + // Build inspector for the params of a shader under an emitter, false if the shader is not under one + public static function editParticleShader(ctx: EditContext2, shader: hrt.prefab.Shader, props: Array, parent: hide.kit.Element) : Bool { + if (getParticleEmitter(shader) == null) + return false; + var shaderDef = shader.getShaderDefinition(); + for (prop in props) { + var v = Lambda.find(shaderDef.data.vars, v -> v.name == prop.name); + var size = EmitterParam.getParamSize(v); + if (size == 0) { + parent.buildProp(prop, shader.props); + continue; + } + var value : Dynamic = Reflect.field(shader.props, prop.name); + var base = [for (i in 0...size) EmitterParam.baseComponent(value, size, i)]; + var t : hrt.prefab.Props.PropType = switch (prop.t) { + case PFloat(_), PVec(_): prop.t; + case PInt(min, max): PFloat(min, max); + default: PFloat(0, 1); } + editParam(ctx, shader, { name: prop.name, t: t, def: base, kind: Particle, groupName: null }, [Constant, Curve], parent); } + return true; + } + + // stops at a trail: its shaders are not particle shaders + static function getParticleEmitter(shader: hrt.prefab.Shader) : Emitter { + var p = shader.parent; + while (p != null && !(p is Emitter) && !(p is hrt.prefab.l3d.Trails)) + p = p.parent; + return Std.downcast(p, Emitter); } + #end -} \ No newline at end of file +} + +#if domkit + +enum abstract ModeKind(String) to String { + var Constant; + var Range; + var Curve; + var RandomCurve; + var CurveRange; +} + +#end diff --git a/hrt/prefab/fx/EmitterMacros.hx b/hrt/prefab/fx/EmitterMacros.hx index 31efb8119..24fc8e189 100644 --- a/hrt/prefab/fx/EmitterMacros.hx +++ b/hrt/prefab/fx/EmitterMacros.hx @@ -3,260 +3,249 @@ package hrt.prefab.fx; #if macro import haxe.macro.Context; import haxe.macro.Expr; - -enum ParamKind { - KScalar; - KVec( n : Int ); - KEnum( e : Expr ); - KGradient; -} - -typedef Param = { - var name : String; - var kind : ParamKind; - var def : Expr; - var pos : Position; - var inst : Bool; - var rand : Bool; -} #end +/** + Emitter fields: + - `@:s` properties, `@:range(min, max)`, `@:animate` when an animation curve can set them, `@:disp("Label", "Description")` (the description is the tooltip of its line), + `@:t(PFile([...]))` for a type other than the one of the field, `@:sameLine` to show it on the line of the previous property + (`@:line("Label")` on the first one names the line, the widgets keep their labels; else the line takes the label of the first one, the others show none); + - `@:param(PVec(3, -10, 10), Particle, default)` params with modes (`EmitterParam`), saved under their name like the properties. + The Particle ones, read over the particle life, are named `partX`; + - `@:group("Name")`: the inspector group of the field and of the next ones. + `FIELDS` has the properties and the params in their declaration order, the one of the inspector. + The random slots are allocated in the declaration order of the params (Emission ones have none). + The evaluator of the emitter (`buildEvaluator`) has a `Value` of the same name for each param and animated property, built by `makeEvaluator`. +**/ class EmitterMacros { - public static macro function param( e : haxe.macro.Expr ) : haxe.macro.Expr { - var name = switch( e.expr ) { - case EConst(CIdent(n)): n; - default: haxe.macro.Context.error("param() expects an emitter param name", e.pos); - } - var decl = macro @:pos(e.pos) $i{"P_" + name}; - var randName = name + "_rand"; - var hasRand = Lambda.exists(haxe.macro.Context.getLocalClass().get().fields.get(), f -> f.name == randName); - var rand = hasRand ? macro @:pos(e.pos) $i{randName} : macro @:pos(e.pos) null; - return switch( haxe.macro.Context.followWithAbstracts(haxe.macro.Context.typeof(e)) ) { - case TInst(_.get().name => "Array", _): macro @:pos(e.pos) vparam($decl, $e, $rand); - default: macro @:pos(e.pos) fparam($decl, $e, $rand); - } - } - #if macro - static function metaObj( f : Field, name : String ) : Null> { - if( f.meta == null ) - return null; + static function meta( f : Field, name : String ) : Array { for( m in f.meta ) - if( m.name == name ) { - if( m.params.length != 1 ) - Context.error(name + " expects one object argument", m.pos); - return switch( m.params[0].expr ) { - case EObjectDecl(fl): fl; - default: Context.error(name + " expects an object argument", m.pos); - } - } + if( m.name == name ) + return m.params; return null; } - static function field( fl : Array, name : String ) : Null { - for( f in fl ) - if( f.field == name ) - return f.expr; - return null; - } - - static function kindOf( t : Expr ) : ParamKind { - return switch( t.expr ) { - case ECall({expr: EConst(CIdent("PVec"))}, args): - switch( args[0].expr ) { - case EConst(CInt(v)): KVec(Std.parseInt(v)); - default: Context.error("PVec size must be a constant", t.pos); - } - case ECall({expr: EConst(CIdent("PEnum"))}, args): KEnum(args[0]); - case EConst(CIdent("PGradient")): KGradient; - default: KScalar; + static function propType( f : Field, t : ComplexType, init : Expr ) : Expr { + var explicit = meta(f, ":t"); + if( explicit != null ) + return explicit[0]; + var r = meta(f, ":range") ?? []; + var min = r[0] ?? macro null, max = r[1] ?? macro null; + return switch( t ) { + case TPath({ name: "Float" }): macro PFloat($min, $max); + case TPath({ name: "Int" }): macro PInt($min, $max); + case TPath({ name: "Bool" }): macro PBool; + case TPath({ name: "String" }): macro PTexture; + case TPath({ name: "Array" }): + switch( init?.expr ) { + case EArrayDecl(v): macro PVec($v{v.length}, $min, $max); + default: Context.error("A vector setting needs its default", f.pos); + } + case TPath({ name: "GradientData" } | { sub: "GradientData" }): macro PGradient; // ?? + case TPath(p): macro PEnum($i{p.sub ?? p.name}); + default: Context.error("Unsupported setting type", f.pos); } } - static function zeroDef( k : ParamKind, pos : Position ) : Expr { - return switch( k ) { - case KVec(n): { expr: EArrayDecl([for( i in 0...n ) macro @:pos(pos) 0.0]), pos: pos }; - default: macro @:pos(pos) 0.0; - } + // a type path from its full name, `pack.Module.Sub` + static function typePath( name : String ) : TypePath { + var parts = name.split("."); + var pack = [while( parts.length > 0 && parts[0].charAt(0) == parts[0].charAt(0).toLowerCase() ) parts.shift()]; + return { pack: pack, name: parts[0], sub: parts[1] }; } - public static function build() : Array { + public static function build( evaluatorType : String ) : Array { var fields = Context.getBuildFields(); var pos = Context.currentPos(); - var params : Array = []; - var emitterDecls : Array = []; - var instDecls : Array = []; - var extra : Array = []; + var fieldDefs = [], params = []; + var load = [], save = [], copy = [], getCases = [], setCases = []; + var values = []; + var group = "Params"; for( f in fields ) { - var inst = false; - var meta = metaObj(f, ":param"); - if( meta == null ) { - meta = metaObj(f, ":instParam"); - inst = meta != null; - } - if( meta == null ) + var g = meta(f, ":group"); + if( g != null ) + group = haxe.macro.ExprTools.getValue(g[0]); + var p = f.pos, n = f.name; + var disp = meta(f, ":disp") ?? []; + var label = disp[0] ?? macro null, desc = disp[1] ?? macro null; + + if( meta(f, ":s") != null ) { + switch( f.kind ) { + case FVar(t, init): + var animate = meta(f, ":animate") != null, sameLine = meta(f, ":sameLine") != null; + var lineMeta = meta(f, ":line"), lineLabel = lineMeta == null ? macro null : lineMeta[0]; + fieldDefs.push(macro @:pos(p) hrt.prefab.fx.EmitterParam.EmitterField.Property({ name: $v{n}, t: ${propType(f, t, init)}, + def: (${init ?? macro null} : Dynamic), groupName: $v{group}, animate: $v{animate}, sameLine: $v{sameLine}, line: $lineLabel, disp: $label, desc: $desc })); + if( init != null && init.expr.match(EArrayDecl(_)) ) + save.push(macro @:pos(p) if( haxe.Json.stringify(this.$n) == haxe.Json.stringify($init) ) Reflect.deleteField(data, $v{n})); + if( animate ) + values.push(macro @:pos(p) v.$n = b.animated($v{n}, hrt.prefab.fx.Evaluator.vVal(this.$n))); + default: + } continue; - - var ft = null, fdef = null; - switch( f.kind ) { - case FVar(t, e): ft = t; fdef = e; - default: Context.error("Emitter param must be a variable", f.pos); } - var t = field(meta, "t"); - if( t == null ) - Context.error("Missing param type", f.pos); - var kind = kindOf(t); - var def = inst ? field(meta, "def") : fdef; - if( def == null ) - def = macro @:pos(f.pos) null; - if( inst ) { - if( fdef != null ) - Context.error("Instance param default belongs in the metadata", f.pos); - f.kind = FVar(macro : Null<$ft>, macro @:pos(f.pos) null); + var a = meta(f, ":param"); + if( a == null ) + continue; + var dim = switch( a[0].expr ) { + case ECall({ expr: EConst(CIdent("PVec")) }, args): Std.parseInt(haxe.macro.ExprTools.toString(args[0])); + default: 1; } - - var decl : Array = [ - { field: "name", expr: macro @:pos(f.pos) $v{f.name} }, - { field: "t", expr: t }, - { field: "def", expr: macro @:pos(f.pos) ($def : Dynamic) }, - ]; - var disp = field(meta, "disp"); - if( disp != null ) decl.push({ field: "disp", expr: disp }); - var group = field(meta, "group"); - if( group != null ) decl.push({ field: "groupName", expr: group }); - decl.push({ field: "animate", expr: inst ? macro true : (field(meta, "animate") ?? macro false) }); - if( inst ) decl.push({ field: "instance", expr: field(meta, "instance") ?? macro true }); - - var declExpr : Expr = { expr: EObjectDecl(decl), pos: f.pos }; - var declName = "P_" + f.name; - extra.push({ - name: declName, - access: [AStatic, AFinal], - kind: FVar(macro : hrt.prefab.fx.EmitterHelper.ParamDef, declExpr), - pos: f.pos, - }); - (inst ? instDecls : emitterDecls).push(macro @:pos(f.pos) $i{declName}); - - params.push({ name: f.name, kind: kind, def: def, pos: f.pos, inst: inst, rand: false }); - - if( inst ) { - extra.push({ - name: f.name + "_rand", - access: [APublic], - kind: FVar(macro : Null<$ft>, macro null), - pos: f.pos, - }); - params.push({ name: f.name + "_rand", kind: kind, def: zeroDef(kind, f.pos), pos: f.pos, inst: true, rand: true }); + var kind = haxe.macro.ExprTools.toString(a[1]); + var defs = switch( a[2]?.expr ) { + case null: [for( _ in 0...dim ) macro 0.]; + case EArrayDecl(values): values; + default: [a[2]]; } + var def = macro @:pos(p) PARAMS[$v{params.length}]; + fieldDefs.push(macro @:pos(p) hrt.prefab.fx.EmitterParam.EmitterField.Param($def)); + params.push(macro @:pos(p) { name: $v{n}, t: ${a[0]}, def: ($a{defs} : Array), kind: hrt.prefab.fx.EmitterParam.ParamKind.$kind, + groupName: $v{group}, disp: $label, desc: $desc }); + if( dim == 1 ) { + load.push(macro @:pos(p) this.$n = hrt.prefab.fx.EmitterParam.parseMode(data.$n)); + save.push(macro @:pos(p) { var v = hrt.prefab.fx.EmitterParam.saveMode(hrt.prefab.fx.EmitterParam.saved(this, $def, 0, this.$n)); if( v != null ) data.$n = v; }); + getCases.push({ values: [macro $v{n}], expr: macro this.$n }); + setCases.push({ values: [macro $v{n}], expr: macro this.$n = m }); + copy.push(macro @:pos(p) this.$n = from.$n); + } else { + // the modes of the components + f.kind = switch( f.kind ) { case FVar(t, _): FVar(t, macro [null, null, null]); default: f.kind; }; + load.push(macro @:pos(p) this.$n = hrt.prefab.fx.EmitterParam.parseVec(data.$n)); + save.push(macro @:pos(p) { var v = hrt.prefab.fx.EmitterParam.saveVec(this, $def, this.$n); if( v != null ) data.$n = v; }); + getCases.push({ values: [macro $v{n}], expr: macro this.$n[comp] }); + setCases.push({ values: [macro $v{n}], expr: macro this.$n[comp] = m }); + copy.push(macro @:pos(p) this.$n = from.$n.copy()); + } + values.push(macro @:pos(p) v.$n = b.param($def)); } - var load : Array = []; - var save : Array = []; - var copy : Array = []; - - for( p in params ) { - var n = p.name; - var def = p.def; - var nullable = p.inst; - var pos = p.pos; - - var src = macro @:pos(pos) props.$n; - load.push(switch( p.kind ) { - case KEnum(e) if( !nullable ): - macro @:pos(pos) this.$n = hrt.prefab.Macros.enumOrNullByName($e, $src, null, $def); - case KGradient: - macro @:pos(pos) this.$n = $src; - case KVec(_): - nullable ? macro @:pos(pos) this.$n = hrt.prefab.fx.EmitterHelper.toArray($src) - : macro @:pos(pos) { var v = $src; this.$n = v == null ? $def : hrt.prefab.fx.EmitterHelper.toArray(v); }; - default: - nullable ? macro @:pos(pos) this.$n = $src - : macro @:pos(pos) { var v = $src; this.$n = v == null ? $def : v; }; - }); - - var set = macro @:pos(pos) Reflect.setField(props, $v{n}, this.$n); - // a set rand keeps its slot even at its neutral value, the editor shows it as set - var changed = if( p.rand ) macro @:pos(pos) true else switch( p.kind ) { - case KVec(_): macro @:pos(pos) !hrt.prefab.fx.EmitterHelper.sameArray(this.$n, $def); - case KGradient: macro @:pos(pos) true; - default: macro @:pos(pos) this.$n != $def; + // out of the editor the values are built once, by the source of the clones (the prefab of a loaded resource): they only depend on + // its data, a load resets them (a resource reload); in the editor the data is edited in place, they are built at each make + var share = !Context.defined("editor") && !Context.defined("editor_hl"); + var reset = share ? macro { sharedValues = null; valuesFrom = null; } : macro {}; + // lazy: the errors of the params come at make, as in the editor + var copyValues = share ? macro { sharedValues = from.sharedValues; valuesFrom = from.sharedValues == null ? from : null; } : macro {}; + var getValues = share ? macro { + if( sharedValues == null ) { + sharedValues = valuesFrom != null ? valuesFrom.getValues() : buildValues(); + valuesFrom = null; } - switch( p.kind ) { - case KEnum(_): set = macro @:pos(pos) Reflect.setField(props, $v{n}, Type.enumConstructor(this.$n)); - default: + return sharedValues; + } : macro return buildValues(); + // each emitter object gets its own evaluator, a game can override its values + var makeEvaluator = share ? macro return getValues().copy() : macro return buildValues(); + var valuesTP = typePath(evaluatorType), valuesCT = TPath(typePath(evaluatorType)); + var selfType = Context.toComplexType(Context.getLocalType()); + // the statics first, the ones of the class can use them + var statics = (macro class { + static var PARAMS : Array = $a{params}; + public static var FIELDS : Array = $a{fieldDefs}; + }).fields; + var methods = (macro class { + function loadParams( data : Dynamic ) { + $reset; + $b{load}; } - save.push(nullable || p.kind.match(KGradient) - ? macro @:pos(pos) if( this.$n != null && $changed ) $set - : macro @:pos(pos) if( $changed ) $set); - - copy.push(switch( p.kind ) { - case KVec(_): macro @:pos(pos) this.$n = from.$n == null ? null : from.$n.copy(); - default: macro @:pos(pos) this.$n = from.$n; - }); + function saveParams( data : Dynamic ) { + $b{save}; + } + function copyParams( from : $selfType ) { + $b{copy}; + $copyValues; + } + public function getFields() return FIELDS; + public function getMode( name : String, comp : Int ) : hrt.prefab.fx.EmitterParam.ParamMode { + return ${{ expr: ESwitch(macro name, getCases, macro null), pos: pos }}; + } + public function setMode( name : String, comp : Int, m : hrt.prefab.fx.EmitterParam.ParamMode ) { + ${{ expr: ESwitch(macro name, setCases, macro throw "No param " + name), pos: pos }}; + } + function buildValues() : $valuesCT { + var b = new hrt.prefab.fx.EmitterParam.ParamBuilder(this); + var v = new $valuesTP(); + $b{values}; + v.randomSlots = b.randomSlots; + return v; + } + function getValues() : $valuesCT { + $getValues; + } + public function makeEvaluator() : $valuesCT { + $makeEvaluator; + } + }).fields; + if( share ) { + methods.push({ name: "sharedValues", access: [], kind: FVar(valuesCT), pos: pos }); + methods.push({ name: "valuesFrom", access: [], kind: FVar(selfType), pos: pos }); } + return statics.concat(fields).concat(methods); + } - var selfType = Context.toComplexType(Context.getLocalType()); - - extra.push({ - name: "emitterParams", - access: [APublic, AStatic], - kind: FVar(macro : Array, { expr: EArrayDecl(emitterDecls), pos: pos }), - pos: pos, - }); - extra.push({ - name: "instanceParams", - access: [APublic, AStatic], - kind: FVar(macro : Array, { expr: EArrayDecl(instDecls), pos: pos }), - pos: pos, - }); - extra.push({ - name: "PARAMS", - access: [APublic, AStatic], - kind: FVar(macro : Map, macro { - var m = new Map(); - for( p in emitterParams ) m.set(p.name, p); - for( p in instanceParams ) m.set(p.name, p); - m; - }), - pos: pos, - }); - - extra.push({ - name: "loadParams", - access: [], - kind: FFun({ - args: [{ name: "props", type: macro : Dynamic }], - expr: macro { if( props == null ) props = {}; $b{load}; }, - }), - pos: pos, - }); - extra.push({ - name: "saveParams", - access: [], - kind: FFun({ - args: [], - ret: macro : Dynamic, - expr: macro { var props : Dynamic = {}; $b{save}; return props; }, - }), - pos: pos, - }); - extra.push({ - name: "copyParams", - access: [], - kind: FFun({ - args: [{ name: "from", type: selfType }], - expr: { expr: EBlock(copy), pos: pos }, - }), - pos: pos, - }); - - for( f in extra ) - fields.push(f); - return fields; + // the evaluator of the emitter `prefabType`, with its values: the Particle params first, read at each update of each particle + public static function buildEvaluator( prefabType : String ) : Array { + var fields = Context.getBuildFields(); + var pos = Context.currentPos(); + var prefab = switch( Context.getType(prefabType) ) { + case TInst(c, _): c.get(); + default: Context.error(prefabType + " is not a class", pos); + } + var particle = [], other = [], getters = []; + for( f in prefab.fields.get() ) { + var n = f.name, get = "get" + n.charAt(0).toUpperCase() + n.substr(1); + var param = f.meta.extract(":param")[0]; + if( param == null ) { + if( f.meta.has(":s") && f.meta.has(":animate") ) { + other.push(n); + getters.push(macro class { public extern inline function $get( t : Float ) : Float return getFloat($i{n}, t); }); + } + continue; + } + var kind = haxe.macro.ExprTools.toString(param.params[1]); + (kind == "Particle" ? particle : other).push(n); + // the Emission ones are read with the random of the emitter (`EmitterHelper`) + if( kind == "Emission" ) + continue; + // a vector: `out` is always set, false when the value is zero + switch( param.params[0].expr ) { + case ECall({ expr: EConst(CIdent("PVec")) }, args) if( haxe.macro.ExprTools.toString(args[0]) == "2" ): + getters.push(macro class { public extern inline function $get( pidx : Int, t : Float, out : h2d.col.Point ) : Bool { + getVector2(pidx, $i{n}, t, out); + return $i{n} != hrt.prefab.fx.Value.VZero; + }}); + case ECall({ expr: EConst(CIdent("PVec")) }, _): + getters.push(macro class { public extern inline function $get( pidx : Int, t : Float, out : h3d.Vector4 ) : Bool { + getFastVec(pidx, $i{n}, t, out); + return $i{n} != hrt.prefab.fx.Value.VZero; + }}); + default: + getters.push(macro class { public extern inline function $get( pidx : Int, t : Float ) : Float return getFast(pidx, $i{n}, t); }); + } + } + var names = particle.concat(other); + for( n in names ) + fields.push({ name: n, access: [APublic], kind: FVar(macro : hrt.prefab.fx.Value), pos: pos }); + for( g in getters ) + fields = fields.concat(g.fields); + var cl = Context.getLocalClass().get(); + var tp : TypePath = { pack: cl.pack, name: cl.module.split(".").pop(), sub: cl.name }; + return fields.concat((macro class { + // per particle + public var randomSlots = 0; + public function new() { + super(); + } + public function copy() { + var v = new $tp(); + $b{[for( n in names ) macro v.$n = $i{n}]}; + v.randomSlots = randomSlots; + return v; + } + }).fields); } #end } diff --git a/hrt/prefab/fx/EmitterParam.hx b/hrt/prefab/fx/EmitterParam.hx new file mode 100644 index 000000000..072a69a0b --- /dev/null +++ b/hrt/prefab/fx/EmitterParam.hx @@ -0,0 +1,294 @@ +package hrt.prefab.fx; + +import hrt.prefab.fx.Value; + +enum ParamKind { + // read at each emission step over the emitter time, random drawn by the emitter + Emission; + // read once per particle at spawn time, over the emitter time, random per particle + Spawn; + // read over the particle life, random per particle + Particle; +} + +enum ParamMode { + Constant( value : Float ); + Range( min : Float, max : Float ); + Curve; + RandomCurve( spreadMin : Float, spreadMax : Float ); + CurveRange; +} + +// the modes of the components of a vector param, x y z +typedef ParamVec2 = Array; +typedef ParamVec3 = Array; + +typedef ParamDef = { + var name : String; + var t : hrt.prefab.Props.PropType; + var def : Array; + var kind : ParamKind; + var groupName : String; + var ?disp : String; + var ?desc : String; +} + +enum EmitterField { + // plain field read as is (can be animated by curves in emitter time) + Property( def : EmitterHelper.PropertyDef ); + // dynamic field with modes (constant, range, curves...), random per particle or per emission cycle + Param( def : ParamDef ); +} + +// see EmitterMacros +interface IEmitter { + function getFields() : Array; + function getMode( name : String, comp : Int ) : ParamMode; + function setMode( name : String, comp : Int, m : ParamMode ) : Void; +} + +class EmitterParam { + + public static var COMPONENTS = ["x", "y", "z", "w"]; + + // the curve of the component `comp` of a param in a curve mode: `name`, `name:x` + public static function curveKey( def : ParamDef, comp : Int ) { + return curveName(def.name, def.def.length, comp); + } + + public static function curveName( name : String, size : Int, comp : Int ) { + return size == 1 ? name : name + ":" + COMPONENTS[comp]; + } + + // the params have their curves in their modes + public static function animKeys( prefab : Prefab, prop : String ) : Array { + var emitter : IEmitter = prefab is IEmitter ? cast prefab : null; + if( emitter == null ) + return null; + return Lambda.exists(emitter.getFields(), f -> switch( f ) { case Property(p): p.animate && p.name == prop; default: false; }) ? [prop] : null; + } + + public static function isCurveMode( m : ParamMode ) { + return switch( m ) { + case Curve, RandomCurve(_), CurveRange: true; + case null, _: false; + } + } + + static function hasCurve( owner : Prefab, name : String ) { + return Lambda.exists(owner.children, c -> c is hrt.prefab.fx.Curve && c.name != null && c.name.toLowerCase() == name.toLowerCase()); + } + + // a curve mode whose curve was deleted is not saved + public static function saved( owner : Prefab, def : ParamDef, i : Int, m : ParamMode ) : ParamMode { + return isCurveMode(m) && !hasCurve(owner, curveKey(def, i)) ? null : m; + } + + // a vector param is saved as { x, y, z }, with only its set components + public static function parseVec( v : Dynamic ) : Array { + return [for( i in 0...3 ) v == null ? null : parseMode(Reflect.field(v, COMPONENTS[i]))]; + } + + public static function saveVec( owner : Prefab, def : ParamDef, v : Array ) : Dynamic { + var o : Dynamic = null; + for( i in 0...def.def.length ) { + var m = saved(owner, def, i, v[i]); + if( m == null ) + continue; + if( o == null ) + o = {}; + Reflect.setField(o, COMPONENTS[i], saveMode(m)); + } + return o; + } + + public static function getParamSize( v : hxsl.Ast.TVar ) : Int { + return switch( v.type ) { + case TVec(n, VFloat) if( v.name.toLowerCase().indexOf("color") < 0 ): n; + case TFloat, TInt, TBool: 1; + default: 0; + } + } + + public static function baseComponent( prop : Dynamic, size : Int, i : Int ) : Float { + var c : Dynamic = size == 1 ? prop : Std.isOfType(prop, Array) ? prop[i] : null; + return Std.isOfType(c, Float) || Std.isOfType(c, Int) ? c : 0.0; + } + + public static function parseMode( v : Dynamic ) : ParamMode { + if( v == null ) + return null; + if( Std.isOfType(v, Float) || Std.isOfType(v, Int) ) + return Constant(v); + inline function num( v : Dynamic, def : Float ) : Float return v ?? def; + return switch( (v.mode : String) ) { + case "Range": Range(num(v.min, 0), num(v.max, 0)); + case "Curve": Curve; + case "RandomCurve": RandomCurve(num(v.spreadMin, 0), num(v.spreadMax, 0)); + case "CurveRange": CurveRange; + default: null; + } + } + + public static function saveMode( m : ParamMode ) : Dynamic { + return switch( m ) { + case null: null; + case Constant(v): v; + case Range(min, max): { mode: "Range", min: min, max: max }; + case Curve: { mode: "Curve" }; + case RandomCurve(spreadMin, spreadMax): + var o : Dynamic = { mode: "RandomCurve" }; + if( spreadMin != 0 ) o.spreadMin = spreadMin; + if( spreadMax != 0 ) o.spreadMax = spreadMax; + o; + case CurveRange: { mode: "CurveRange" }; + } + } + + static function isSavedMode( v : Dynamic ) { + return Std.isOfType(v, Float) || Std.isOfType(v, Int) || Reflect.hasField(v, "mode"); + } + + public static function loadShaderModes( v : Dynamic ) : Map { + if( v == null ) + return null; + var out = new Map(); + for( name in Reflect.fields(v) ) { + var p : Dynamic = Reflect.field(v, name); + if( isSavedMode(p) ) { + var m = parseMode(p); + if( m != null ) out.set(name, m); + } else + for( c in COMPONENTS ) { + var m = parseMode(Reflect.field(p, c)); + if( m != null ) out.set(name + ":" + c, m); + } + } + return out; + } + + public static function saveShaderModes( modes : Map ) : Dynamic { + if( modes == null ) + return null; + var keys = [for( k in modes.keys() ) k]; + if( keys.length == 0 ) + return null; + keys.sort(Reflect.compare); + var out : Dynamic = {}; + for( k in keys ) { + var parts = k.split(":"); + if( parts.length == 1 ) { + Reflect.setField(out, k, saveMode(modes.get(k))); + continue; + } + var v : Dynamic = Reflect.field(out, parts[0]); + if( v == null ) + Reflect.setField(out, parts[0], v = {}); + Reflect.setField(v, parts[1], saveMode(modes.get(k))); + } + return out; + } + + // spread + slope * v: spreadMin at the lowest value of the curve, spreadMax at the highest + public static function spreadLine( range : { min : Float, max : Float }, spreadMin : Float, spreadMax : Float ) : { spread : Float, slope : Float } { + var d = range.max - range.min; + if( d <= 1e-9 * Math.max(1, Math.abs(range.max)) ) + return { spread: spreadMin, slope: 0 }; + var slope = (spreadMax - spreadMin) / d; + return { spread: spreadMin - slope * range.min, slope: slope }; + } + + public static function paramCurveRange( pc : ParamCurve ) : { min : Float, max : Float } { + var r = pc.c1.getValueRange(); + if( pc.c2 != null ) { + var r2 = pc.c2.getValueRange(); + r = { min: Math.min(r.min, r2.min), max: Math.max(r.max, r2.max) }; + } + return r; + } +} + +class ParamBuilder { + + var owner : Prefab; + public var randomSlots(default, null) = 0; + // the random of an Emission param is drawn by the emitter: no slot + var withSlots = true; + + public function new( owner : Prefab ) { + this.owner = owner; + } + + public static function curve( owner : Prefab, name : String, def : Float ) : Value { + return hrt.prefab.fx.Curve.getCurve(owner, name)?.makeVal() ?? Evaluator.vVal(def); + } + + static function fixedValue( owner : Prefab, m : ParamMode, name : String, def : Float ) : Value { + return switch( m ) { + case null: Evaluator.vVal(def); + case Constant(v): Evaluator.vVal(v); + case Curve: curve(owner, name, def); + default: throw "Random mode " + m + " not supported here"; + } + } + + inline function slot() { + return withSlots ? randomSlots++ : -1; + } + + public function comp( m : ParamMode, name : String, def : Float ) : Value { + return switch( m ) { + case Range(min, max): VRandom(slot(), min, max); + case RandomCurve(spreadMin, spreadMax): + var rid = slot(); + switch( curve(owner, name, def) ) { + case VCurve(c): + var l = EmitterParam.spreadLine(c.getValueRange(), spreadMin, spreadMax); + VRandomCurve(rid, c, l.spread, l.slope); + case VParamCurve(pc): + var l = EmitterParam.spreadLine(EmitterParam.paramCurveRange(pc), spreadMin, spreadMax); + VRandomParamCurve(rid, pc, l.spread, l.slope); + case v: + // a flat curve: the spread at its lowest value + var k = switch( v ) { case VConst(k): k; default: 0.; }; + VRandom(rid, k - spreadMin, k + spreadMin); + } + case CurveRange: + var c = rangeCurve(owner, name); + c == null ? Evaluator.vVal(def) : VCurveRange(slot(), c.datas[0], c.datas[1]); + case null, _: fixedValue(owner, m, name, def); + } + } + + public function animated( name : String, base : Value ) : Value { + return hrt.prefab.fx.Curve.getCurve(owner, name)?.makeVal() ?? base; + } + + function mode( def : ParamDef, i : Int ) : ParamMode { + return (cast owner : IEmitter).getMode(def.name, i); + } + + // the curve of a Curve Range, its two datas the bounds + static function rangeCurve( owner : Prefab, name : String ) : hrt.prefab.fx.Curve { + var c = hrt.prefab.fx.Curve.getCurve(owner, name); + if( c != null && c.blendMode != hrt.prefab.fx.Curve.CurveBlendMode.Range ) + throw owner.getAbsPath(true) + ': a Curve Range needs a curve in Range mode'; + return c; + } + + function component( def : ParamDef, i : Int ) : Value { + return comp(mode(def, i), EmitterParam.curveKey(def, i), def.def[i]); + } + + public function param( def : ParamDef ) : Value { + withSlots = def.kind != Emission; + if( def.def.length == 1 ) + return component(def, 0); + var v = VVector(component(def, 0), component(def, 1), def.def.length > 2 ? component(def, 2) : VZero); + return switch( v ) { + case VVector(VZero, VZero, VZero): VZero; + case VVector(VConst(1.0), VConst(1.0), VConst(1.0)): VConst(1.0); + default: v; + } + } +} diff --git a/hrt/prefab/fx/Evaluator.hx b/hrt/prefab/fx/Evaluator.hx index d85eda351..721c9feef 100644 --- a/hrt/prefab/fx/Evaluator.hx +++ b/hrt/prefab/fx/Evaluator.hx @@ -1,13 +1,13 @@ package hrt.prefab.fx; +import hrt.prefab.fx.Value; + class Evaluator { var randValues : Array; public var parameters: Map = []; - var stride : Int; + var slots = 0; - public function new(?randValues: Array, stride: Int=0) { - this.randValues = randValues; - this.stride = stride; + public function new() { } public static function vVal(f: Float) : Value { @@ -17,61 +17,18 @@ class Evaluator { } } - static function canRemap(v: Value) : Bool { - return switch(v) { - case VZero, VConst(_), VRandom(_): true; // only values that we are sure do not use time - default: false; - } + public function allocRandom(count: Int, slots: Int) { + this.slots = slots; + randValues = [for(_ in 0...count * slots) 0.]; } - static inline function commute(a: Value, b: Value, fn: (Value, Value) -> Null) : Null { - var r = fn(a, b); - return r != null ? r : fn(b, a); - } - - public static function vMult(a: Value, b: Value) : Value { - if(a == VZero || b == VZero) return VZero; - var r = commute(a, b, (a, b) -> switch [a, b] { - case [VConst(1.0), _]: b; - case [VConst(va), VConst(vb)]: VConst(va * vb); - case [VConst(va), VCurve(c)]: VCurveT(c, va, 0.0); - case [VConst(va), VCurveT(c, scale, add)]: VCurveT(c, scale * va, add * va); - case [VRandom(rid, scale, add), VCurve(c)]: VRandMultCurve(rid, scale, add, c); - case [VRandom(rid, scale, 0.0), VCurveT(c, cscale, 0.0)]: VRandMultCurve(rid, scale * cscale, 0.0, c); - case [VRandom(rid, scale, add), VBlendCurves(1.0, 0.0, ca, cb, bv)]: - VRandomBlendCurves(rid, scale, add, ca, cb, bv); - case [VConst(va), VBlendCurves(scale, add, ca, cb, bv)]: - VBlendCurves(scale * va, add * va, ca, cb, bv); - case [VConst(va), VRandomBlendCurves(rid, scale, add, ca, cb, bv)]: - VRandomBlendCurves(rid, scale * va, add * va, ca, cb, bv); - case [_, VParamRemap(v, param)] if(canRemap(a)): VParamRemap(vMult(a, v), param); - case [_, VValueRemap(v, remap)] if(canRemap(a)): VValueRemap(vMult(a, v), remap); - default: null; - }); - return r != null ? r : VMult(a, b); - } - - public static function vAdd(a: Value, b: Value) : Value { - if(a == VZero) return b; - if(b == VZero) return a; - var r = commute(a, b, (a, b) -> switch [a, b] { - case [VConst(va), VConst(vb)]: VConst(va + vb); - case [VConst(va), VRandom(rid, scale, add)]: VRandom(rid, scale, add + va); - case [VConst(va), VCurve(c)]: VCurveT(c, 1.0, va); - case [VConst(va), VCurveT(c, scale, add)]: VCurveT(c, scale, add + va); - case [VRandom(rid, scale, add), VCurve(c)]: VRandAddCurve(rid, scale, add, c); - case [VRandom(rid, scale, add), VCurveT(c, 1.0, cadd)]: VRandAddCurve(rid, scale, add + cadd, c); - case [VConst(va), VBlendCurves(scale, add, ca, cb, bv)]: VBlendCurves(scale, add + va, ca, cb, bv); - case [_, VParamRemap(v, param)] if(canRemap(a)): VParamRemap(vAdd(a, v), param); - case [_, VValueRemap(v, remap)] if(canRemap(a)): VValueRemap(vAdd(a, v), remap); - default: null; - }); - return r != null ? r : VAdd(a, b); - } - - inline function getRandom(pidx: Int, ridx: Int) { - var i = pidx * stride + ridx; - return randValues[i]; + public function resetRandom(rand: hxd.Rand) { + for(i in 0...randValues.length) + randValues[i] = (rand.srand() + 1) * 0.5; + } + + inline function getRandom(pidx: Int, rid: Int) { + return randValues[pidx * slots + rid]; } public function setAllParameters(params: Array) { @@ -83,106 +40,83 @@ class Evaluator { } } - public function getFast(pidx: Int, val: Value, time: Float) : Float { + public function getAtRandom(val: Value, time: Float, u: Float) : Float { + return eval(val, time, _ -> u); + } + + public inline function getMean(val: Value, time: Float) : Float { + return eval(val, time, _ -> 0.5); + } + + public inline function getFloat(val: Value, time: Float) : Float { + return getFast(0, val, time); + } + + // no optional argument, causes boxing on HL + public inline function getFast(pidx: Int, val: Value, time: Float) : Float { + return eval(val, time, rid -> getRandom(pidx, rid)); + } + + inline function eval(val: Value, time: Float, rand: Int -> Float) : Float { return switch(val) { - case null: 0.0; - case VZero: 0.0; + case null, VZero: 0.0; case VConst(v): v; + case VRandom(rid, min, max): hxd.Math.lerp(min, max, rand(rid)); case VCurve(c): c.getVal(time); - case VCurveT(c, scale, add): c.getVal(time) * scale + add; - case VRandom(rid, scale, add): getRandom(pidx, rid) * scale + add; - case VRandMultCurve(rid, scale, add, c): - (getRandom(pidx, rid) * scale + add) * c.getVal(time); - case VRandAddCurve(rid, scale, add, c): - getRandom(pidx, rid) * scale + add + c.getVal(time); - case VBlendCurves(scale, add, a, b, blendVar): - var va = a.getVal(time); - var vb = b.getVal(time); - hxd.Math.lerp(va, vb, parameters[blendVar] ?? 0.0) * scale + add; - case VRandomBlendCurves(rid, scale, add, a, b, blendVar): - var rnd = getRandom(pidx, rid) * scale + add; - var va = a.getVal(time); - var vb = b.getVal(time); - rnd * hxd.Math.lerp(va, vb, parameters[blendVar] ?? 0.0); - case VRandomBetweenCurves(rid, a, b): - var va = a.getVal(time); - var vb = b.getVal(time); - va + (vb - va) * ((getRandom(pidx, rid) + 1) * 0.5); - case VParamRemap(a, param): - getFast(pidx, a, parameters[param] ?? 0.0); - case VValueRemap(a, remap): - getFast(pidx, a, getFast(pidx, remap, time)); - default: - throw "getFast: non-flat Value " + Type.enumConstructor(val); + case VParamCurve(pc): getParamCurve(pc, time); + case VRandomCurve(rid, c, spread, slope): + var v = c.getVal(time); + v + (spread + slope * v) * (rand(rid) * 2 - 1); + case VRandomParamCurve(rid, pc, spread, slope): + var v = getParamCurve(pc, time); + v + (spread + slope * v) * (rand(rid) * 2 - 1); + case VCurveRange(rid, a, b): hxd.Math.lerp(a.getVal(time), b.getVal(time), rand(rid)); + case VVector(_), VColor(_), VHsl(_): throw "getFloat: vector value"; } } - public function getFloat(pidx: Int=0, val: Value, time: Float) : Float { - switch(val) { - case VParamRemap(a, param): - var time = parameters[param] ?? 0.0; - return getFloat(pidx, a, time); - case VValueRemap(a, remap): - var time = getFloat(pidx, remap, time); - return getFloat(pidx, a, time); - case VAdd(a, b): - return getFloat(pidx, a, time) + getFloat(pidx, b, time); - case VMult(a, b): - return getFloat(pidx, a, time) * getFloat(pidx, b, time); - default: - return getFast(pidx, val, time); - } + function getParamCurve(pc: ParamCurve, time: Float) : Float { + var p = parameters[pc.param] ?? 0.0; + return pc.c2 != null ? hxd.Math.lerp(pc.c1.getVal(time), pc.c2.getVal(time), p) : pc.c1.getVal(p); } public function getSum(val: Value, time: Float) : Float { - switch(val) { - case VZero: return 0; - case VConst(v): return v * time; - case VCurve(c): return c.getSum(time); - case VCurveT(c, scale, add): return c.getSum(time) * scale + add * time; - case VAdd(a, b): - return getSum(a, time) + getSum(b, time); - case VParamRemap(a, param): - var blend = parameters[param] ?? 0.0; - return getSum(a, blend) * time; - case VMult(a, VConst(b)), VMult(VConst(b), a): return getSum(a, time) * b; - case VBlendCurves(scale, add, a, b, blendVar): - var blend = parameters[blendVar] ?? 0.0; - return hxd.Math.lerp(a.getSum(time), b.getSum(time), blend) * scale + add * time; - case VRandomBlendCurves(rid, scale, add, a, b, blendVar): - var blend = parameters[blendVar] ?? 0.0; - return (getRandom(0, rid) * scale + add) * hxd.Math.lerp(a.getSum(time), b.getSum(time), blend); + return switch(val) { + case VZero: 0; + case VConst(v): v * time; + case VCurve(c): c.getSum(time); + case VParamCurve(pc): + var p = parameters[pc.param] ?? 0.0; + pc.c2 != null ? hxd.Math.lerp(pc.c1.getSum(time), pc.c2.getSum(time), p) : pc.c1.getVal(p) * time; default: throw "not implemented"; } - return 0.0; } public function getVector(pidx: Int=0, v: Value, time: Float, vec: h3d.Vector4) { switch(v) { - case VMult(a, b): - throw "need optimization"; - case VVector(x, y, z, null): - vec.set(getFloat(pidx, x, time), getFloat(pidx, y, time), getFloat(pidx, z, time), 1.0); - case VVector(x, y, z, w): - vec.set(getFloat(pidx, x, time), getFloat(pidx, y, time), getFloat(pidx, z, time), getFloat(pidx, w, time)); + case VVector(x, y, z): + vec.set(getFast(pidx, x, time), getFast(pidx, y, time), getFast(pidx, z, time), 1.0); + case VColor(r, g, b, a): + vec.set(getFast(pidx, r, time), getFast(pidx, g, time), getFast(pidx, b, time), getFast(pidx, a, time)); case VHsl(h, s, l, a): - vec.makeColor(getFloat(pidx, h, time), getFloat(pidx, s, time), getFloat(pidx, l, time)); - vec.a = getFloat(pidx, a, time); + vec.makeColor(getFast(pidx, h, time), getFast(pidx, s, time), getFast(pidx, l, time)); + vec.a = getFast(pidx, a, time); case VZero: vec.set(0,0,0,1); default: - var f = getFloat(pidx, v, time); + var f = getFast(pidx, v, time); vec.set(f, f, f, 1.0); } return vec; } + // inlined, `vec` stays on the stack public inline function getFastVec(pidx: Int, v: Value, time: Float, vec: h3d.Vector4) { switch(v) { - case VVector(x, y, z, null): + case VVector(x, y, z): vec.set(getFast(pidx, x, time), getFast(pidx, y, time), getFast(pidx, z, time), 1.0); - case VVector(x, y, z, w): - vec.set(getFast(pidx, x, time), getFast(pidx, y, time), getFast(pidx, z, time), getFast(pidx, w, time)); + case VColor(r, g, b, a): + vec.set(getFast(pidx, r, time), getFast(pidx, g, time), getFast(pidx, b, time), getFast(pidx, a, time)); case VZero: vec.set(0,0,0,1); default: @@ -194,16 +128,12 @@ class Evaluator { public function getVector2(pidx: Int=0, v: Value, time: Float, vec: h2d.col.Point) { switch(v) { - case VMult(a, b): - throw "need optimization"; - case VVector(x, y, z, null): - vec.set(getFloat(pidx, x, time), getFloat(pidx, y, time)); - case VVector(x, y, z, w): - vec.set(getFloat(pidx, x, time), getFloat(pidx, y, time)); + case VVector(x, y, _): + vec.set(getFast(pidx, x, time), getFast(pidx, y, time)); case VZero: vec.set(0,0); default: - var f = getFloat(pidx, v, time); + var f = getFast(pidx, v, time); vec.set(f, f); } return vec; diff --git a/hrt/prefab/fx/FX.hx b/hrt/prefab/fx/FX.hx index a9e832103..605b0d6a3 100644 --- a/hrt/prefab/fx/FX.hx +++ b/hrt/prefab/fx/FX.hx @@ -1,6 +1,5 @@ package hrt.prefab.fx; import hrt.prefab.fx.BaseFX.AdditionalProperies; -import hrt.prefab.Curve; import hrt.prefab.Prefab as PrefabElement; import hrt.prefab.fx.BaseFX.ObjectAnimation; import hrt.prefab.fx.BaseFX.ShaderAnimation; @@ -125,7 +124,7 @@ class FXAnimation extends h3d.scene.Object { updateCustomAnims(root); events = initEvents(root, events); - var root = hrt.prefab.fx.BaseFX.BaseFXTools.getFXRoot(def); + var root = BaseFXTools.getFXRoot(def); initConstraints(root != null ? root : def); trails = findAll((p) -> Std.downcast(p, hrt.prefab.l3d.Trails.TrailObj)); @@ -578,86 +577,65 @@ class FXAnimation extends h3d.scene.Object { } static var tempMat = new h3d.Matrix(); - static var tempTransform = new h3d.Matrix(); static var tempVec = new h3d.Vector4(); - function syncAnims(newTime: Float, dt: Float) { + function syncAnims(t: Float, dt: Float) { if(objAnims != null) { for(anim in objAnims) { if(anim.obj != null && (anim.scale != null || anim.rotation != null || anim.position != null)) { + var e = anim.elt; + inline function eval(v: Value, base: Float) return v == null ? base : evaluator.getFloat(v, t); var m = tempMat; if(anim.scale != null) { - var scale = evaluator.getVector(anim.scale, newTime, tempVec); - m.initScale(scale.x, scale.y, scale.z); + var s = anim.scale; + if(s.x != null && s.x == s.y && s.x == s.z) { + var sc = evaluator.getFloat(s.x, t); // Optim: get curve only once for uniform scales + m.initScale(sc, sc, sc); + } else { + var sx = eval(s.x, e.scaleX); + var sy = eval(s.y, e.scaleY); + var sz = eval(s.z, e.scaleZ); + m.initScale(sx, sy, sz); + } } else - m.identity(); - - if(anim.rotation != null) { - var rotation = evaluator.getVector(anim.rotation, newTime, tempVec); - rotation.scale3(Math.PI / 180.0); - m.rotate(rotation.x, rotation.y, rotation.z); - } - - var baseMat = anim.elt.getTransform(tempTransform); - var offset = baseMat.getPosition(); - baseMat.tx = baseMat.ty = baseMat.tz = 0.0; // Ignore - m.multiply(baseMat, m); - m.translate(offset.x, offset.y, offset.z); - - if(anim.position != null) { - var pos = evaluator.getVector(anim.position, newTime, tempVec); - m.translate(pos.x, pos.y, pos.z); - } - + m.initScale(e.scaleX, e.scaleY, e.scaleZ); + + inline function radians(deg: Float) return hxd.Math.degToRad(deg); + var rot = anim.rotation; + if(rot != null) + m.rotate( + radians(eval(rot.x, e.rotationX)), + radians(eval(rot.y, e.rotationY)), + radians(eval(rot.z, e.rotationZ))); + else if(e.rotationX != 0 || e.rotationY != 0 || e.rotationZ != 0) + m.rotate(radians(e.rotationX), radians(e.rotationY), radians(e.rotationZ)); + + var pos = anim.position; + if(pos != null) + m.translate(eval(pos.x, e.x), eval(pos.y, e.y), eval(pos.z, e.z)); + else + m.translate(e.x, e.y, e.z); anim.obj.setTransform(m); } - // Animations that are only applied on local transforms of leafs objects - if (anim.localRotation != null || anim.localPosition != null) { - var leafObjects = anim.elt.findAll(Object3D, o -> o.children == null || o.children.length == 0); - - for (o in leafObjects) { - if (o.local3d == null) - continue; - var baseMat = o.getTransform(); - - tempMat.identity(); - var m = tempMat; - - if(anim.localRotation != null) { - var localRotation = evaluator.getVector(anim.localRotation, newTime, tempVec); - localRotation.scale3(Math.PI / 180.0); - m.rotate(localRotation.x, localRotation.y, localRotation.z); - } - - if(anim.localPosition != null) { - var localPosition = evaluator.getVector(anim.localPosition, newTime, tempVec); - m.translate(localPosition.x, localPosition.y, localPosition.z); - } - - m.multiply(m, baseMat); - o.local3d.setTransform(m); - } - } - if(anim.visibility != null) { var visible = anim.elt.visible; #if editor var editor = anim.elt.shared.editor; visible = visible && (editor?.isVisible(anim.elt) ?? true); #end - anim.obj.visible = visible && evaluator.getFloat(anim.visibility, newTime) > 0.5; + anim.obj.visible = visible && evaluator.getFloat(anim.visibility, t) > 0.5; } if(anim.color != null) { switch(anim.color) { - case VCurve(a): + case VCurve(_) | VParamCurve(_): for(mat in anim.obj.getMaterials()) - mat.color.a = evaluator.getFloat(anim.color, newTime); + mat.color.a = evaluator.getFloat(anim.color, t); default: for(mat in anim.obj.getMaterials()) - mat.color.load(evaluator.getVector(anim.color, newTime, tempVec)); + mat.color.load(evaluator.getVector(anim.color, t, tempVec)); } } @@ -668,33 +646,33 @@ class FXAnimation extends h3d.scene.Object { var l = Std.downcast(anim.obj, h3d.scene.pbr.PointLight); if( l != null ) { if( color != null ) { - var v = evaluator.getVector(color, newTime, tempVec); + var v = evaluator.getVector(color, t, tempVec); l.color.set(v.x, v.y, v.z); } - if( power != null ) l.power = evaluator.getFloat(power, newTime); - if( size != null ) l.size = evaluator.getFloat(size, newTime); - if( range != null ) l.range = evaluator.getFloat(range, newTime); + if( power != null ) l.power = evaluator.getFloat(power, t); + if( size != null ) l.size = evaluator.getFloat(size, t); + if( range != null ) l.range = evaluator.getFloat(range, t); } case DirLight(color, power): var l = Std.downcast(anim.obj, h3d.scene.pbr.DirLight); if( l != null ) { if( color != null ) { - var v = evaluator.getVector(color, newTime, tempVec); + var v = evaluator.getVector(color, t, tempVec); l.color.set(v.x, v.y, v.z); } - if( power != null ) l.power = evaluator.getFloat(power, newTime); + if( power != null ) l.power = evaluator.getFloat(power, t); } case SpotLight(color, power, range, angle, fallOff): var l = Std.downcast(anim.obj, h3d.scene.pbr.SpotLight); if( l != null ) { if( color != null ) { - var v = evaluator.getVector(color, newTime, tempVec); + var v = evaluator.getVector(color, t, tempVec); l.color.set(v.x, v.y, v.z); } - if( power != null ) l.power = evaluator.getFloat(power, newTime); - if( range != null ) l.range = evaluator.getFloat(range, newTime); - if( angle != null ) l.angle = evaluator.getFloat(angle, newTime); - if( fallOff != null ) l.fallOff = evaluator.getFloat(fallOff, newTime); + if( power != null ) l.power = evaluator.getFloat(power, t); + if( range != null ) l.range = evaluator.getFloat(range, t); + if( angle != null ) l.angle = evaluator.getFloat(angle, t); + if( fallOff != null ) l.fallOff = evaluator.getFloat(fallOff, t); } } } @@ -752,7 +730,18 @@ class FXAnimation extends h3d.scene.Object { var obj3d = elt.to(hrt.prefab.Object3D); if(obj3d == null) return; + var anim = makeObjectAnimation(obj3d, Object3D.getLocal3d(elt)); + if(anim != null) { + if(objAnims == null) objAnims = []; + objAnims.push(anim); + } + } + static function makeObjectAnimation(obj3d: hrt.prefab.Object3D, local3d: h3d.scene.Object) : ObjectAnimation { + var elt : PrefabElement = obj3d; + // most objects have no curve: none of the lookups below would find one + if(!Lambda.exists(elt.children, c -> c.enabled && Std.isOfType(c, Curve))) + return null; var anyFound = false; function makeVal(name, def) : Value { @@ -765,31 +754,16 @@ class FXAnimation extends h3d.scene.Object { return c.makeVal(); } - function makeVector(name: String, defVal: Float, uniform: Bool=true, scale: Float=1.0) : Value { - var curves = Curve.getCurves(elt, name); - if(curves == null || curves.length == 0) - return null; - - anyFound = true; - - if(uniform && curves.length == 1 && curves[0].name == name) { - return scale != 1.0 ? Evaluator.vMult(curves[0].makeVal(), VConst(scale)) : curves[0].makeVal(); - } - - return Curve.getVectorValue(curves, defVal, scale); - } - function makeColor(name: String) { var curves = Curve.getCurves(elt, name); if(curves == null || curves.length == 0) return null; anyFound = true; - return Curve.getColorValue(curves); + return BaseFXTools.getColorValue(curves); } var ap : AdditionalProperies = null; - var local3d = Object3D.getLocal3d(elt); if( Std.isOfType(local3d, h3d.scene.pbr.PointLight)) { ap = PointLight(makeColor("color"), makeVal("power", null), makeVal("size", null), makeVal("range", null) ); @@ -804,20 +778,14 @@ class FXAnimation extends h3d.scene.Object { var anim : ObjectAnimation = { elt: obj3d, obj: local3d, - position: makeVector("position", 0.0), - localPosition: makeVector("localPosition", 0.0), - scale: makeVector("scale", 1.0, true), - rotation: makeVector("rotation", 0.0, 360.0), - localRotation: makeVector("localRotation", 0.0, 360.0), + position: BaseFXTools.vectorAnimation(elt, "position"), + scale: BaseFXTools.vectorAnimation(elt, "scale"), + rotation: BaseFXTools.vectorAnimation(elt, "rotation"), color: makeColor("color"), visibility: makeVal("visibility", null), additionalProperies: ap, }; - - if(anyFound) { - if(objAnims == null) objAnims = []; - objAnims.push(anim); - } + return anyFound || anim.position != null || anim.scale != null || anim.rotation != null ? anim : null; } function initEmitters(elt: PrefabElement) { @@ -933,10 +901,56 @@ class FX extends Object3D implements BaseFX { cullingRadius = 3.0; } + /** + Version of the data of an FX, saved on its root (and on an emitter out of any FX), 0 when missing. + Older data is upgraded on load with `-D fx-migration`, or rejected. + **/ + public static inline var VERSION = 2; + + /** Whether `p` is in FX data: under an FX, or under an emitter out of any FX (the root of its FX data) **/ + public static function inFXData( p : Prefab ) { + return p.findParent(Prefab, c -> c is BaseFX || c is Emitter || c is Emitter2D) != null; + } + + /** Called before `p`, the root of FX data, loads `data` **/ + public static function checkVersion( p : Prefab, data : Dynamic ) { + var v : Dynamic = data.version; + if( v == VERSION ) + return; + var version : Int = v == null ? 0 : v; + #if fx_migration + if( hide.tools.migrate.Migrate.canUpgrade(version) ) { + hide.tools.migrate.Migrate.fallback(data, version, p.parent == null); + return; + } + #end + throw '${p.shared.currentPath} ${p.getAbsPath()}: FX data of version $version, this build reads version $VERSION. ' + + 'Migrate the project with the hide migration tool (hide/migrate.hxml), or upgrade it on load with -D fx-migration'; + } + + public static function inFX( p : Prefab ) { + return p.findParent(Prefab, c -> c is BaseFX, true) != null; + } + + override function load( data : Dynamic ) { + checkVersion(this, data); + super.load(data); + } + + override function copy( obj : Prefab ) { + super.copy(obj); + } + + override function save() : Dynamic { + var data = super.save(); + data.version = VERSION; + return data; + } + override function make( ?sh:hrt.prefab.Prefab.ContextMake) : Prefab { var fromRef = shared.parentPrefab != null; var useFXRoot = #if editor fromRef #else true #end; - var root = hrt.prefab.fx.BaseFX.BaseFXTools.getFXRoot(this); + var root = BaseFXTools.getFXRoot(this); if(useFXRoot && root != null){ var childrenBackup = _children; _children = [root]; @@ -985,7 +999,7 @@ class FX extends Object3D implements BaseFX { } public override function postMakeInstance() { - var root = hrt.prefab.fx.BaseFX.BaseFXTools.getFXRoot(this); + var root = BaseFXTools.getFXRoot(this); var fxAnim = Std.downcast(local3d, FXAnimation); if (fxAnim == null) return; @@ -1081,11 +1095,11 @@ class FX extends Object3D implements BaseFX { var to = isUndo ? old : v; p.name = to; - // rename all curves that used this param as a blendParam + // rename all curves driven by this param var curves = flatten(Curve); for (c in curves) { - if (c.blendMode == Blend && c.blendParam == from) { - c.blendParam = to; + if (c.param == from) { + c.param = to; } } diff --git a/hrt/prefab/fx/FX2D.hx b/hrt/prefab/fx/FX2D.hx index 43b2fa175..fbd9541cd 100644 --- a/hrt/prefab/fx/FX2D.hx +++ b/hrt/prefab/fx/FX2D.hx @@ -1,8 +1,8 @@ package hrt.prefab.fx; -import hrt.prefab.Curve; import hrt.prefab.Prefab as PrefabElement; import hrt.prefab.fx.BaseFX.ObjectAnimation; import hrt.prefab.fx.BaseFX.ShaderAnimation; +import hrt.prefab.fx.BaseFX.BaseFXTools; import hrt.prefab.fx.Event; @:allow(hrt.prefab.fx.FX2D) @@ -104,35 +104,30 @@ class FX2DAnimation extends h2d.Object { } - static var tmpPt = new h3d.Vector4(); public function setTime( time : Float ) { this.localTime = time; for(anim in objects) { - if(anim.scale != null) { - evaluator.getVector(anim.scale, time, tmpPt); - anim.obj2d.scaleX = anim.elt2d.scaleX * tmpPt.x; - anim.obj2d.scaleY = anim.elt2d.scaleY * tmpPt.y; + var e = anim.elt2d, o = anim.obj2d, p = anim.position, s = anim.scale, r = anim.rotation; + inline function f(v: Value, base: Float) return v == null ? base : evaluator.getFloat(v, time); + if(p != null) { + o.x = f(p.x, e.x); + o.y = f(p.y, e.y); } - - if(anim.rotation != null) { - var rotation = evaluator.getFloat(anim.rotation, time); - anim.obj2d.rotation = anim.elt2d.rotation + rotation * (Math.PI / 180.0); - } - - if(anim.position != null) { - evaluator.getVector(anim.position, time, tmpPt); - anim.obj2d.x = anim.elt2d.x + tmpPt.x; - anim.obj2d.y = anim.elt2d.y + tmpPt.y; + if(s != null) { + o.scaleX = f(s.x, e.scaleX); + o.scaleY = f(s.y, e.scaleY); } + if(r != null) + o.rotation = hxd.Math.degToRad(f(r.x, e.rotation)); if(anim.visibility != null) anim.obj2d.visible = anim.elt2d.visible && evaluator.getFloat(anim.visibility, time) > 0.5; if(anim.color != null) { switch(anim.color) { - case VCurve(a): + case VCurve(_) | VParamCurve(_): anim.obj2d.alpha = evaluator.getFloat(anim.color, time); default: var drawable = Std.downcast(anim.obj2d, h2d.Drawable); @@ -210,6 +205,21 @@ class FX2D extends Object2D implements BaseFX { @:s var loop : Bool = false; @:s var startLoop : Float = 0.0; + override function load( data : Dynamic ) { + FX.checkVersion(this, data); + super.load(data); + } + + override function copy( obj : Prefab ) { + super.copy(obj); + } + + override function save() : Dynamic { + var data = super.save(); + data.version = FX.VERSION; + return data; + } + function getObjAnimations(elt: PrefabElement, anims: Array) { for(c in elt.children) { @@ -219,7 +229,13 @@ class FX2D extends Object2D implements BaseFX { var obj2d = elt.to(hrt.prefab.Object2D); if(obj2d == null || obj2d.local2d == null) return; + var anim = makeObjectAnimation(obj2d, obj2d.local2d); + if(anim != null) + anims.push(anim); + } + static function makeObjectAnimation(obj2d: hrt.prefab.Object2D, local2d: h2d.Object) : ObjectAnimation { + var elt : PrefabElement = obj2d; // TODO: Support references? var anyFound = false; @@ -227,20 +243,7 @@ class FX2D extends Object2D implements BaseFX { var c = Curve.getCurve(elt, name); if(c != null) anyFound = true; - return c != null ? VCurve(c) : def; - } - - function makeVector(name: String, defVal: Float, uniform: Bool=true, scale: Float=1.0) : Value { - var curves = Curve.getCurves(elt, name); - if(curves == null || curves.length == 0) - return null; - - anyFound = true; - - if(uniform && curves.length == 1 && curves[0].name == name) - return scale != 1.0 ? VMult(curves[0].makeVal(), VConst(scale)) : curves[0].makeVal(); - - return Curve.getVectorValue(curves, defVal, scale); + return c != null ? c.makeVal() : def; } function makeColor(name: String) { @@ -249,24 +252,23 @@ class FX2D extends Object2D implements BaseFX { return null; anyFound = true; - return Curve.getColorValue(curves); + return BaseFXTools.getColorValue(curves); } var anim : ObjectAnimation = { elt2d: obj2d, - obj2d: obj2d.local2d, - position: makeVector("position", 0.0), - scale: makeVector("scale", 1.0, true), - rotation: makeVector("rotation", 0.0, 360.0), + obj2d: local2d, + position: BaseFXTools.vectorAnimation(elt, "position"), + scale: BaseFXTools.vectorAnimation(elt, "scale"), + rotation: BaseFXTools.vectorAnimation(elt, "rotation"), color: makeColor("color"), visibility: makeVal("visibility", null), }; - if (Std.isOfType(elt, hrt.prefab.l2d.Anim2D) || Std.isOfType(elt, hrt.prefab.l2d.Atlas)) + if (anim.position != null || anim.scale != null || anim.rotation != null || Std.isOfType(elt, hrt.prefab.l2d.Anim2D) || Std.isOfType(elt, hrt.prefab.l2d.Atlas)) anyFound = true; - if(anyFound) - anims.push(anim); + return anyFound ? anim : null; } override function make( ?sh:hrt.prefab.Prefab.ContextMake) : Prefab { @@ -275,7 +277,7 @@ class FX2D extends Object2D implements BaseFX { #else var fromRef = shared.parentPrefab != null; var useFXRoot = #if editor fromRef #else true #end; - var root = hrt.prefab.fx.BaseFX.BaseFXTools.getFXRoot(this); + var root = BaseFXTools.getFXRoot(this); if( useFXRoot && root != null ) { var childrenBackup = _children; _children = [root]; @@ -292,7 +294,7 @@ class FX2D extends Object2D implements BaseFX { var fxanim : FX2DAnimation = cast local2d; fxanim.init(this); getObjAnimations(this, fxanim.objects); - hrt.prefab.fx.BaseFX.BaseFXTools.getCustomAnimations(this, fxanim.customAnims); + BaseFXTools.getCustomAnimations(this, fxanim.customAnims); } override function makeObject(parent2d: h2d.Object) : h2d.Object { diff --git a/hrt/prefab/fx/Value.hx b/hrt/prefab/fx/Value.hx index 6a507a798..85e629cef 100644 --- a/hrt/prefab/fx/Value.hx +++ b/hrt/prefab/fx/Value.hx @@ -1,31 +1,29 @@ package hrt.prefab.fx; +@:structInit class ParamCurve { + public var c1 : hrt.impl.Curve; + public var c2 : hrt.impl.Curve; + public var param : String; // FX parameter +} + enum Value { - // fast flat values VZero; VConst(v: Float); - VCurve(c: Curve); - // c * scale + add - VCurveT(c: Curve, scale: Float, add: Float); - // random(rid) * scale + add - VRandom(rid: Int, scale: Float, add: Float); - // (random(rid) * scale + add) * c - VRandMultCurve(rid: Int, scale: Float, add: Float, c: Curve); - // random(rid) * scale + add + c - VRandAddCurve(rid: Int, scale: Float, add: Float, c: Curve); - // lerp(a.getVal(t), b.getVal(t), blendVar) * scale + add - VBlendCurves(scale: Float, add: Float, a: Curve, b: Curve, blendVar: String); - // (random(rid) * scale + add) * lerp(a.getVal(t), b.getVal(t), blendVar) - VRandomBlendCurves(rid: Int, scale: Float, add: Float, a: Curve, b: Curve, blendVar: String); - // lerp(a.getVal(t), b.getVal(t), (random(rid)+1)/2) - VRandomBetweenCurves(rid: Int, a: Curve, b: Curve); - - // rare complex values that we cannot currently optimize - VParamRemap(a: Value, param: String); - VValueRemap(v: Value, remap: Value); - VAdd(a: Value, b: Value); - VMult(a: Value, b: Value); + // lerp(min, max, u) + VRandom(rid: Int, min: Float, max: Float); + // c(t), c with absolute values for keys + VCurve(c: hrt.impl.Curve); + // lerp(pc.c1(t), pc.c2(t), pc.param) + VParamCurve(pc: ParamCurve); + // v + (spread + slope * v) * (2u - 1), v = c(t), c with absolute values for keys: the random spread follows the value of the + // curve (`EmitterParam.spreadLine`, from the spreads of the mode at the lowest and the highest value of the curve) + VRandomCurve(rid: Int, c: hrt.impl.Curve, spread: Float, slope: Float); + // same as VRandomCurve with the value of VParamCurve + VRandomParamCurve(rid: Int, pc: ParamCurve, spread: Float, slope: Float); + // lerp(a(t), b(t), u), a and b with absolute values for keys + VCurveRange(rid: Int, a: hrt.impl.Curve, b: hrt.impl.Curve); - VVector(x: Value, y: Value, z: Value, ?w: Value); + VVector(x: Value, y: Value, z: Value); + VColor(r: Value, g: Value, b: Value, a: Value); VHsl(h: Value, s: Value, l: Value, a: Value); } diff --git a/hrt/prefab/fx/gpuemitter/RelativeTransformSimulation.hx b/hrt/prefab/fx/gpuemitter/RelativeTransformSimulation.hx index 420b833b9..21d2bc551 100644 --- a/hrt/prefab/fx/gpuemitter/RelativeTransformSimulation.hx +++ b/hrt/prefab/fx/gpuemitter/RelativeTransformSimulation.hx @@ -140,7 +140,7 @@ class RelativeTransformSimulation extends SimulationShader { s.curveTexture = new h3d.mat.Texture(width, height, null, R32F); - var curves = findAll(hrt.prefab.Curve, p -> p.shouldBeInstanciated()); + var curves = findAll(hrt.prefab.fx.Curve, p -> p.shouldBeInstanciated()); if ( curves.length == 0 ) return; diff --git a/hrt/prefab/l2d/Particle2D.hx b/hrt/prefab/l2d/Particle2D.hx index bd37bb420..bbe8c02c0 100644 --- a/hrt/prefab/l2d/Particle2D.hx +++ b/hrt/prefab/l2d/Particle2D.hx @@ -138,8 +138,8 @@ class Particle2D extends Object2D { } function makeVal(name, def ) : Value { - var c = Curve.getCurve(this, name); - return c != null ? VCurve(c) : def; + var c = hrt.prefab.fx.Curve.getCurve(this, name); + return c != null ? c.makeVal() : def; } if (paramsParticleGroup != null) { @@ -177,7 +177,7 @@ class Particle2D extends Object2D { #if editor - public static var emitter2dParams : Array = [ + public static var emitter2dParams : Array = [ // EMIT PARAMS { name: "enable", t: PBool, disp: "Enable", def : 1.0, animate: true, groupName : "Emit Params" }, { name: "speed", t: PFloat(), disp: "Initial Speed", def : 1.0, animate: true, groupName : "Emit Params" }, diff --git a/hrt/shader/ParticleShader.hx b/hrt/shader/ParticleShader.hx new file mode 100644 index 000000000..b7e0d2e5c --- /dev/null +++ b/hrt/shader/ParticleShader.hx @@ -0,0 +1,49 @@ +package hrt.shader; + +/** + Base shader for Particles, following the general direction of reducing the size of the shader list +**/ +class ParticleShader extends hxsl.Shader { + + static var SRC = { + @:import hrt.shader.BaseEmitter; + + @const var randomMode : hrt.prefab.fx.Emitter.RandomColorMode; + @param var randomColor1 : Vec4; + @param var randomColor2 : Vec4; + @param var randomGradient : Sampler2D; + + @const var fadeMode : hrt.prefab.fx.Emitter.RandomColorMode; + @param var fadeColor1 : Vec4; + @param var fadeColor2 : Vec4; + @param var fadeGradient : Sampler2D; + + @const var alphaFade : Bool; + @param var fadeIn : Float; + @param var fadeOut : Float; + + function gradientUV( t : Float, width : Float ) : Vec2 { + return vec2((t * (width - 1.0) + 0.5) / width, 0.5); + } + + function fragment() { + if( randomMode == Colors ) + pixelColor *= mix(randomColor1, randomColor2, particleRandom); + else if( randomMode == Gradient ) + pixelColor *= randomGradient.get(gradientUV(particleRandom, randomGradient.size().x)); + + var t = saturate(particleLife / particleLifeTime); + if( fadeMode == Colors ) + pixelColor *= mix(fadeColor1, fadeColor2, t); + else if( fadeMode == Gradient ) + pixelColor *= fadeGradient.get(gradientUV(t, fadeGradient.size().x)); + + if( alphaFade ) { + var a = 1.0; + if( fadeIn > 0. ) a = min(a, t / fadeIn); + if( fadeOut > 0. ) a = min(a, (1.0 - t) / fadeOut); + pixelColor.a *= saturate(a); + } + } + }; +} diff --git a/hrt/ui/HuiCurveBox.hx b/hrt/ui/HuiCurveBox.hx index 292ef8e4c..ef9f660a2 100644 --- a/hrt/ui/HuiCurveBox.hx +++ b/hrt/ui/HuiCurveBox.hx @@ -19,7 +19,7 @@ class HuiKeyPopup extends HuiPopup { - public function new(k : hrt.prefab.Curve.CurveKey, editor : HuiCurveEditor, ?parent: h2d.Object) { + public function new(k : hrt.impl.Curve.CurveKey, editor : HuiCurveEditor, ?parent: h2d.Object) { super(parent); initComponent(); @@ -110,9 +110,13 @@ class HuiCurveBox extends HuiElement { graphics.lineStyle(CURVE_WIDTH, CURVE_COLOR, 1); + var maxValue = value.data.getValueRange().max; + if (maxValue <= 0) + maxValue = 1; + graphics.moveTo(0, 0); for (idx => v in value.sample(CURVE_PRECISION)) - graphics.lineTo((idx / CURVE_PRECISION) * this.calculatedWidth, (v / value.maxValue) * -1 * this.calculatedHeight); + graphics.lineTo((idx / CURVE_PRECISION) * this.calculatedWidth, (v / maxValue) * -1 * this.calculatedHeight); } public dynamic function onValueChanged(isTemporary: Bool) {} @@ -558,12 +562,12 @@ class HuiCurveEditor extends HuiPopup { inline function addPrevH() { if(key.prevHandle == null) - key.prevHandle = new hrt.prefab.Curve.CurveHandle(prev != null ? (prev.time - key.time) / 3 : -0.5, 0); + key.prevHandle = new hrt.impl.Curve.CurveHandle(prev != null ? (prev.time - key.time) / 3 : -0.5, 0); } inline function addNextH() { if(key.nextHandle == null) - key.nextHandle = new hrt.prefab.Curve.CurveHandle(next != null ? (next.time - key.time) / 3 : -0.5, 0); + key.nextHandle = new hrt.impl.Curve.CurveHandle(next != null ? (next.time - key.time) / 3 : -0.5, 0); } switch(key.mode) { @@ -585,8 +589,6 @@ class HuiCurveEditor extends HuiPopup { key.time = 0; // if(maxLength > 0 && key.time > maxLength) // key.time = maxLength; - if(key.time > value.maxTime) - key.time = value.maxTime; if(prev != null && key.time < prev.time) key.time = prev.time + 0.01; if(next != null && key.time > next.time) diff --git a/migrate.hxml b/migrate.hxml new file mode 100644 index 000000000..9099e38d7 --- /dev/null +++ b/migrate.hxml @@ -0,0 +1,22 @@ +# The FX data migration tool, usage: hl migrate.hl --help +# What a prefab animates, and from which values, depends on its class: a type the build does not have is +# not upgraded (an error). A game with its own prefab classes builds the tool from its own hxml instead, with +# its libs, class paths and prefab packages, e.g. from the game dir: +# haxe common.hxml -D fx-migration -main hide.tools.migrate.Cli -hl migrate.hl +-cp . +-lib heaps +-lib hscript +-lib domkit +-D hide +-D hscriptPos +-D fx-migration +-D resourcesPath=hide_hl/emptyres +-main hide.tools.migrate.Cli +-hl migrate.hl +--macro keep('h3d.shader') +--macro include("hrt.prefab", false) +--macro include("hrt.prefab.fx") +--macro include("hrt.prefab.l3d", false) +--macro include("hrt.prefab.l2d", false) +--macro include("hrt.prefab.rfx", false) +--macro include("hrt.shader", false)