diff --git a/bin/hidekit.css b/bin/hidekit.css index 58bd311d6..924a53573 100644 --- a/bin/hidekit.css +++ b/bin/hidekit.css @@ -43,8 +43,216 @@ kit-root { max-height: 500px; overflow: auto; } -} + kit-div.kit-param-comp > kit-text { + flex: 0 0 auto; + min-width: 10px; + margin-left: 3px; + } + + 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 +279,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 +297,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 +416,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 +535,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 +814,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 +849,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/emitter-spec.md b/emitter-spec.md new file mode 100644 index 000000000..d1d5a9065 --- /dev/null +++ b/emitter-spec.md @@ -0,0 +1,100 @@ +# Emitter params — spec + +Scope: `Emitter` and `Emitter2D`. Editing through the new inspector only (edit2 / HideKit), in the JS editor (hide_js); HideHL is dropped. Colors (particle shader color curves, emitter random color / gradient) keep their current model. + +## Terms + +- **Particle curve**: a curve over the life of a particle, time in [0, 1]. Edited inline in the inspector, shown in the timeline only when it is selected. +- **Animation curve**: a curve over the FX time, or over the emitter time for the ones of an emitter (its properties and Spawn params: the FX time minus its start delay, scaled by its time scale, plus its warm up). Shown and edited in the timeline. + +## Param groups + +| Group | Params | Modes | Timeline | +|---|---|---|---| +| Emitter | emission settings (rate, burst count, angle, shape dimensions...), scale / orbit speed over time | Constant | animatable ones take an animation curve, read once per tick (the over time ones apply to all the live particles) | +| Spawn | lifetime, start speed, start world speed | Constant, Range | `value` / `min` / `max` take an animation curve, read when the particle spawns | +| Particle | speed, world speed, acceleration, world acceleration, orbit speed, dampen, max velocity, scale, stretch, stretch velocity, rotation, pivot, offset | all | — | +| Particle shader params | params of the shaders applied to the particles | Constant, Curve (random modes later) | — | + +Vector params have one mode per component. Rotations are in degrees, rotation speeds (orbit speed) in degrees per second. +Particle shaders get their per-particle randomness on the GPU, from the emitter's `particleRandom`. +A shader under an emitter lists its components read over the particle life in `particleParams`, by the name of their curve (`alpha`, `UVOffset:y`), a curve of the shader that gives the value; a component without curve keeps the prop. + +## Modes + +| Mode | Label | Fields | Value | +|---|---|---|---| +| unset | Default value | — | default value of the param | +| `Constant` | Constant | `value` | `value` | +| `Range` | Range | `min`, `max` | `lerp(min, max, u)` | +| `Curve` | Curve | `shape`, `from`, `to` | `lerp(from, to, s(t))` | +| `RandomCurve` | Random Curve | `shape`, `from: {min, max}`, `to: {min, max}` | `lerp(lerp(from.min, from.max, u), lerp(to.min, to.max, u), s(t))` | +| `CurveRange` | Curve Range | `a`, `b`, each `{shape, from, to}`, two plain curves | `lerp(a(t), b(t), u)` | + +- `t`: particle life in [0, 1]. `s(t)`: the shape evaluated at `t`. +- `u`: a random in [0, 1], one draw per particle and per param component, keyed by the param. Every random field of a mode uses the same draw. +- `from` / `to` default to 0 / 1. +- Unset and the default value are the same: an unset param shows its default value grayed out, editing it sets it, and a constant set back to its default (by hand or by a reset) is unset. + +## Shapes + +A shape is a **curve**: the child curve of the emitter named after the param component (`partScale`, `partRotation:z`), the first enabled one, as every curve of an emitter. A curve is one of: plain keys (with loop); a blend of two plain curves by an FX parameter; plain keys remapped by an FX parameter (read at its value instead of the time); a reference to one of these (resolved when the values are built, its scale and offset folded into the range). + +The range (`from` / `to`) belongs to the param; a reference shares the shape only. +Rejected when the values are built (an error), and not offered by the curve inspector: a remap by another curve, a remapped blend or reference, a blend of curves that are not plain, a curve by a parameter in Curve Range. + +## Animation curves + +- They set the animated value directly. +- They hold plain keys: no range, no random. Constants of the old data are baked into the keys. They take the same curve forms as the shapes. +- A Spawn param is read when the particle spawns: its value, or a random between its `min` and `max`, both at the emitter time. +- Targets: transforms, shader params outside emitters, Emitter params, Spawn `value` / `min` / `max`. +- Animation curves are children of the animated prefab, found by name (the property name, `:x` / `:y` / `:z` per component, e.g. `position:x`, `emitRate`, `startSpeed:x:min`), the first enabled one. The inspector marks properties that have one. + +## Storage + +- Params are stored directly on the emitter, under their field name, like the properties. The name tells the clock of the curve: the Particle params, read over the particle life, are prefixed with `part` (`partScale`, `partRotation`); the Spawn params and the properties, read over the emitter time, are not (`lifeTime`, `startSpeed`, `emitRate`). A param is a mode, or for a vector an object of the modes of its components (`x`, `y`, `z`, an unset one left out). A curve mode stores no curve, its curve is the one of its name; a curve mode whose curve was deleted is not saved (the param goes back to its default). +- Old files are converted once by a migration tool in `hide/tools`, run over a whole project: it rewrites every old-format file, updates the files that override or point at what it changes, only touches the nodes it changes, and is idempotent. Loading an old-format file still works as a fallback: the same migration code upgrades it in memory, alone (no fixes in other files), with a warning recommending the tool. +- Every FX file carries a format `version` at its root (no version = 0, the old format); everything inside the file, including its Reference overrides, follows it. Upgrade steps are keyed by version. A project is always migrated as a whole. + +## Runtime + +- The emitter object reads its settings from its prefab (`prefab`, the clone made for this instance), when it uses them. It holds only the values built from the params and the animated properties (`emitRate`, `lifeTime`, `partScale`...) and its state (`emitScale`, `enable`, the particles). +- Game code overrides a built value on the object (`e.emitRate = VConst(10)`, which replaces its curve too), and changes a setting on the prefab (`e.prefab.randomColor1 = [r, g, b, 1]`). A setting read at init (max count, sprite sheet, particle template) has no effect when changed after the make. A new `updateInstance` rebuilds the values, dropping their overrides. + +## Upgrade + +Rule: runtime-equivalent whenever possible, while simplifying. + +`k`: base value, `a`: random amount (`_rand`), `c`: curve. + +| Old | New | +|---|---| +| `k` | Constant `k` | +| `k ± a` | Range `[k−a, k+a]` | +| `k·c` | Curve `c`, from 0, to `k` | +| `(k ± a)·c` | RandomCurve `c`, from `{0, 0}`, to `{k−a, k+a}` | +| `k ± a + c` (rotation, offset, pivot) | RandomCurve `c`, from `{k−a, k+a}`, to `{k−a+1, k+a+1}` | +| random between two curves | CurveRange, both curves with from 0, to 1 | +| curve blend / time remap by a parameter / loop | kept on the curve | +| time remap by another curve, remapped blend or reference, blend of curves that are not plain | migration error | +| lifetime + lifetime random | Spawn, Range | +| start speed curves (emitter time) | animation curves on its Spawn fields, baked | +| scale / orbit speed over time | Emitter settings, Constant or animation curve | +| emitter param `k·c` | animation curve, `k` baked | +| object transform curves | animation curves overriding the transform, exact affine bakes of the old ones (rotation curves in turns become degrees): the base rotation folded into the euler angles (from another euler triple of the same rotation, then stored on the object), the animated scale moved before the base rotation (components permuted); a rotation the euler curves cannot follow gets a parent object with the original name, which takes the animation as it is, the object keeping its base transform as `_base`, which the look at and constraint targets now name; a scale the base rotation shears, and such a rotation on an object that cannot get a parent (a look at with a lock axis, a billboard, an emitter, a swarm, the direct children of a shader target, swarm or GPU emitter, an object that would become a leaf of a local animation), are sampled to what the old runtime showed (`setTransform`: the shear dropped, the quaternion normalized, the previous rotation kept while a scale is 0), with a migration warning | +| orbit speed (radians per second) | degrees per second | +| particle shader float `c·k` | Curve `c`, from 0, to `k` | +| particle shader vector, some components animated | animated components: Curve; the others: Constant 0 | + +Validation (fxbench, outside the repo): old hide on the old files against new hide on the migrated files, over the games' .fx: param values as canonical formulas (constants rounded, randoms compared by structure), animation curves sampled over time, object transforms simulated over time, and rendered sheets. + +## UI + +- The right click menu of a param component (on its field, or on the label of its line) switches its mode, restricted to the modes of its group, and removes it (**Remove X**) or the whole param (**Remove all**). The components of a vector are on one line while they are all constant or unset. +- Switching to a curve mode makes the curve of the param component (or gives the existing one its new values), switching away from them removes it. +- A curve mode made from a constant, a range or nothing gets a new linear curve, from 0 to 1. +- Curve modes show their particle curves in an inline curve editor. Below the editor, on one line, the `from` fields at the left (the birth of the particle) and the `to` ones at the right (its death), a button between them swapping them. Random Curve: the shape at mid range as a solid line, the two extremes dashed. Keys are edited in display space, as in the timeline curve editor: key modes (Aligned, Free, Linear, Constant) and tangent handles, Ctrl + click adds a key, Delete removes it, right click for the key menu, double click for its time and value. The editor has a labelled Y grid in integer steps (0 stronger) and shows at least 0 to 1: framed on the curve when built or with F, zoomed vertically around its center with its +/- buttons or Shift + wheel. +- Curve Range: two stacked curve editors. +- The FX timeline shows the animation curves only, and a particle curve when it is selected (over the particle life, 0 to 1). +- In the FX editor, animated properties are marked in the inspector, Blender-like; the right click menu of an animatable field animates it and inserts or removes keys. 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..bdc47bc0e 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 { @@ -696,6 +746,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 +786,8 @@ class Element { } if (widget != null) { widget.label = def.disp ?? hide.kit.Macros.camelToSpaceCase(def.name); + widget.tooltip = def.desc; + widget.anim = def.name; 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..1dab39e20 --- /dev/null +++ b/hide/tools/migrate/Upgrade0.hx @@ -0,0 +1,2246 @@ +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 + params are named after what they animate (`startSpeed:x:min`, `scaleMultiplier`), 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); + } + + /** `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 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); + } + + /** + 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); + 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 ) { + 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, "random amount following a curve around a constant value, a curve of two datas"); + } + + /** + 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.animKey`). 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; + } + if( isBlend(c) ) { + // random between the two child curves, the value is not used: a range, its bounds the child curves + var sub = c.curveChildren(); + if( sub.length < 2 ) { + warn(c, "random blend without its two child curves"); + continue; + } + var min = newCurve(freeName(e, key + ":min")); + min.keys = datum(sub[0].data).keys; + if( get(sub[0].data, "loop") == true ) min.loop = true; + e.addChild(min); + flattenBlend(c, sub[1]); + out[i] = MRange(0, 0); + changed = true; + renames.push({ node: c, name: key + ":max" }); + change(c, "spawn, random blend: a range"); + continue; + } + if( bounds == null ) { + renames.push({ node: c, name: key }); + bakeLinear(c, k, 0); + change(c, "spawn value"); + continue; + } + // (k ± a) * c: one curve per bound + var lo = bounds.lo, hi = bounds.hi; + if( hi != 0 ) { + renames.push({ node: c, name: key + ":max" }); + // a reference to `c`, compensated by the bake of `c` + if( lo != 0 ) + linearCopy(c, e, key + ":min", lo, 0); + bakeLinear(c, hi, 0); + } else if( lo != 0 ) { + renames.push({ node: c, name: key + ":min" }); + bakeLinear(c, lo, 0); + } + change(c, "spawn range"); + } + 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 curves read over the particle life, named after their param and component (`EmitterParam.curveName`) + var keys = [], renames = []; + for( name => v in out ) + for( i => m in v ) + switch( bakeModeCurves(s, m) ) { + case MCurve(path, _): + var key = v.length == 1 ? name : name + ":" + COMPONENTS[i]; + keys.push(key); + var n = locate(path, s); + if( n != null && n.name != key ) + renames.push({ node: n, name: key }); + case null, _: + } + renameCurves(renames); + keys.sort(Reflect.compare); + RawNode.setField(s.data, "particleParams", keys); + 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"]); + if( f1 != f0 ) o.spreadMin = (f1 - f0) * 0.5; + if( t1 != t0 ) o.spreadMax = (t1 - t0) * 0.5; + 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 13735ad25..6567b900e 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 isEmitter = hrt.prefab.fx.EmitterHelper.isEmitter(elt); 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,13 @@ 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(isEmitter) { + for(param in hrt.prefab.fx.EmitterHelper.getProperties(elt)) + if(param.animate) + addParam(param, ""); + for(p in hrt.prefab.fx.EmitterHelper.getParams(elt)) + if(p.kind != Particle) + addParam({ name: p.name, t: p.t, def: p.def.length > 1 ? p.def : p.def[0], disp: p.disp }, ""); } if (particle2dElt != null) { @@ -1684,6 +1544,7 @@ class FXEditor extends hide.view.FileView { onUpdate2D(dt); else onUpdate3D(dt); + sceneEditor.curEdit2?.root?.refreshAnimatedFields(); } function onUpdate2D(dt:Float) { @@ -1849,7 +1710,43 @@ 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 = hrt.prefab.fx.EmitterHelper.isEmitter(element); + var def = Lambda.find(hrt.prefab.fx.EmitterHelper.getParams(element), p -> p.name == parts[0]); + if(def != null) { + var i = parts.length > 1 ? hxd.Math.imax(0, hrt.prefab.fx.EmitterParam.COMPONENTS.indexOf(parts[1])) : 0; + var isMin = parts[parts.length - 1] == "min"; + return switch(hrt.prefab.fx.EmitterHelper.getMode(element, def.name, i)) { + case Range(min, max): isMin ? min : max; + case Constant(v): v; + case null, _: def.def[i]; + } + } + 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) { @@ -1857,10 +1754,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/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..c0da3b60c 100644 --- a/hrt/prefab/Shader.hx +++ b/hrt/prefab/Shader.hx @@ -19,6 +19,9 @@ class Shader extends Prefab { public var shader : hxsl.Shader; + /** The curves of its params read over the life of the particles, under an emitter (`EmitterParam.curveName`) **/ + @:s public var particleParams : Array = []; + function new(parent, sh: ContextShared) { super(parent, sh); props = {}; @@ -293,7 +296,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 e9b3544a1..3c9f1545a 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,100 @@ 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 { + var keys = [for(i in 0...size) EmitterParam.curveName(name, size, i)]; + if(!Lambda.exists(keys, k -> shader.particleParams.contains(k))) + return null; + var prop : Dynamic = Reflect.field(shader.props, name); + function comp(i: Int) { + var base = EmitterParam.baseComponent(prop, size, i); + return shader.particleParams.contains(keys[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,37 +302,43 @@ 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") }; + } + 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 elt.children) { - 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..9bf803252 --- /dev/null +++ b/hrt/prefab/fx/Curve.hx @@ -0,0 +1,409 @@ +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 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); + } + } + + 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; + if (!EmitterHelper.isEmitter(parent)) + return rootCurve.findParent(Prefab, p -> EmitterHelper.isEmitter(p)) != null; + return !isChannel(rootCurve.name) && !isParamName(parent, 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: Prefab, name: String) { + var base = name?.split(":")[0]; + return Lambda.exists(EmitterHelper.getProperties(emitter), p -> p.animate == true && p.name == base) || Lambda.exists(EmitterHelper.getParams(emitter), 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 c2b5bb044..ac2a13acb 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; @@ -73,26 +72,6 @@ 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() { } -} - typedef PartArray = #if (hl_ver >= version("1.14.0")) hl.CArray #else Array #end; typedef Single = #if (hl_ver >= version("1.13.0")) hl.F32 #else Float #end; @@ -244,13 +223,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 +274,23 @@ 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.getFastVec(idx, emitter.partStretch, t, v); + v.scale3(evaluator.getFast(idx, emitter.partScale, 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(emitter.partPivot != VZero) { + evaluator.getFastVec(idx, emitter.partPivot, 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(emitter.partRotation != VZero) { + evaluator.getFastVec(idx, emitter.partRotation, 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 +299,8 @@ class ParticleInstance { } //OFFSET - if(def.localOffset != VZero) { - evaluator.getFastVec(idx, def.localOffset, t, v); + if(emitter.partOffset != VZero) { + evaluator.getFastVec(idx, emitter.partOffset, t, v); localMat.tx += v.x; localMat.ty += v.y; localMat.tz += v.z; @@ -341,35 +320,33 @@ 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.getSpawnVec(idx, emitter.startSpeed, emitter.curTime, v); v.transform3x3(emitOrientation); add(speedAccumulation, v); - // START WORLD SPEED - evaluator.getFastVec(idx, emitter.startWorldSpeed, emitter.curTime, v); + // world speed + evaluator.getSpawnVec(idx, emitter.startWorldSpeed, emitter.curTime, v); v.transform3x3(emitter.invTransform); add(speedAccumulation, v); } - // ACCELERATION - if(def.acceleration != VZero) { - evaluator.getFastVec(idx, def.acceleration, t, v); + if(emitter.partAcceleration != VZero) { + evaluator.getFastVec(idx, emitter.partAcceleration, 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(emitter.partWorldAcceleration != VZero) { + evaluator.getFastVec(idx, emitter.partWorldAcceleration, t, v); v.scale3(dt); - if(emitter.simulationSpace == Local) + if(prefab.simulationSpace == Local) v.transform3x3(emitter.invTransform); add(speedAccumulation, v); } @@ -378,32 +355,27 @@ class ParticleInstance { speed.y += speedAccumulation.y; speed.z += speedAccumulation.z; - // SPEED - if(def.localSpeed != VZero) { - evaluator.getFastVec(idx, def.localSpeed, t, v); + if(emitter.partSpeed != VZero) { + evaluator.getFastVec(idx, emitter.partSpeed, t, v); v.transform3x3(emitOrientation); add(speed, v); } - // DAMPEN - if (def.dampen != VZero) { - var dampen = evaluator.getFast(idx, def.dampen, t); + if (emitter.partDampen != VZero) { + var dampen = evaluator.getFast(idx, emitter.partDampen, 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(emitter.partWorldSpeed != VZero) { + evaluator.getFastVec(idx, emitter.partWorldSpeed, 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 (emitter.partMaxSpeed != VZero) { + var maxVel = evaluator.getFast(idx, emitter.partMaxSpeed, t); var curVelSq = speed.lengthSq(); if (maxVel * maxVel < curVelSq) { speed.normalize(); @@ -411,15 +383,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 (emitter.partStretchVelocity != VZero) { + var s = evaluator.getFast(idx, emitter.partStretchVelocity, 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 +410,11 @@ class ParticleInstance { y += speed.y * dt; z += speed.z * dt; - if(def.orbitSpeed != VZero) { - evaluator.getFastVec(idx, def.orbitSpeed, t, v); + if(emitter.partOrbitSpeed != VZero) { + evaluator.getFastVec(idx, emitter.partOrbitSpeed, 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 +436,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 +460,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); @@ -519,13 +489,62 @@ class ParticleInstance { @:allow(hrt.prefab.fx.Emitter) class EmitterObject extends h3d.scene.Object { - public var instDef : InstanceDef; + public var prefab : Emitter; + + // Emission + public var emitRate : Value; + public var emitRateMin : Value; + public var emitRateMax : Value; + public var burstParticleCount : Value; + public var lifeTime : SpawnValue; + public var emitScale : Float = 1.0; // multiplier of the emission, set by the game + public var emitRateChangeStart : Float = 0.0; + public var emitRateCurrent : Float = Math.NaN; + public var emitRatePrevious : Float = Math.NaN; + public var emitRateTarget : Float = Math.NaN; + public var totalBurstCount : Int = 0; + + // Emit Shape + public var emitAngle : Value; + + // Particle Movement + public var startSpeed : Array; + public var startWorldSpeed : Array; + public var partSpeed : Value; + public var partWorldSpeed : Value; + public var partAcceleration : Value; + public var partWorldAcceleration : Value; + public var partOrbitSpeed : Value; + public var orbitSpeedMultiplier : Value; + + // Limit Velocity + public var partDampen : Value; + public var partMaxSpeed : Value; + + // Particle Transform + public var partOffset : Value; + public var partPivot : Value; + public var partRotation : Value; + public var partScale : Value; + public var scaleMultiplier : Value; + public var partStretch : Value; + public var partStretchVelocity : Value; + + // Ground Collision + var collisionPlane : h3d.col.Plane; + + // made from the children of the prefab + public var particleTemplate : hrt.prefab.Object3D; + public var subEmitterTemplates : Array; + public var subEmitters : Array; + public var trails : hrt.prefab.l3d.Trails.TrailObj; + public var trailsTemplate : hrt.prefab.l3d.Trails; 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 +556,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,82 +575,7 @@ class EmitterObject extends h3d.scene.Object { } #end - public var emitterPrefab : Emitter; public var parentNonEmitter : h3d.scene.Object; - - // RANDOM - public var seedGroup = 0; - // OBJECTS - 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; - 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(); public var worldScale = new h3d.Vector(1,1,1); @@ -647,6 +590,9 @@ class EmitterObject extends h3d.scene.Object { var emitCount = 0; var emitTarget = 0.0; var curTime = 0.0; + // the values of the over time settings at the current tick, shared by the particles + var curScaleMultiplier = 1.0; + var curOrbitSpeedMultiplier = 1.0; var evaluator : Evaluator; var numInstances = 0; var instanceCounter = 0; @@ -659,8 +605,6 @@ class EmitterObject extends h3d.scene.Object { 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); @@ -701,8 +645,12 @@ class EmitterObject extends h3d.scene.Object { } function init(randSlots: Int, prefab: Emitter) { - this.emitterPrefab = prefab; - this.randSlots = randSlots; + this.prefab = prefab; + evaluator = new Evaluator(); + evaluator.allocRandom(prefab.maxCount, randSlots); + var anim = getParentFX(); + if(anim != null) + evaluator.parameters = @:privateAccess anim.evaluator.parameters; updateParentNonEmitter(); @@ -740,7 +688,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 +733,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(lifeTime); + 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,7 +764,7 @@ class EmitterObject extends h3d.scene.Object { baseEmitterShader.meshTransformInverse.initInverse(meshTransform); batch.material.mainPass.addShader(baseEmitterShader); - if(useRandomColor) { + if(prefab.randomColorMode != RandomColorMode.None) { colorMultShader = new h3d.shader.ColorMult(); batch.material.mainPass.addShader(colorMultShader); } @@ -829,20 +778,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(); } @@ -886,15 +828,12 @@ class EmitterObject extends h3d.scene.Object { softReset(); random.init(randomSeed); numInstances = 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) { @@ -967,7 +906,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; @@ -976,7 +915,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; } @@ -1046,7 +985,8 @@ class EmitterObject extends h3d.scene.Object { if( count == 0 ) return; - if( instDef == null) + // the values are not made yet + if( lifeTime == null ) return; @:privateAccess var globalTime = scene?.renderer.ctx.globalTime ?? curTime; @@ -1055,7 +995,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) ) @@ -1067,45 +1007,43 @@ 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.getSpawn(part.idx, lifeTime, curTime)); + + switch(prefab.randomColorMode) { + case None: + case Gradient: + part.colorMult = Gradient.evalData(prefab.randomGradient, random.rand()).toColor(); + case Colors: + var c1 = prefab.randomColor1, c2 = prefab.randomColor2, k = random.rand(); + part.colorMult = new h3d.Vector(hxd.Math.lerp(c1[0], c2[0], k), hxd.Math.lerp(c1[1], c2[1], k), hxd.Math.lerp(c1[2], c2[2], k)).toColor(); } 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 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 { @@ -1114,14 +1052,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; @@ -1136,10 +1074,10 @@ 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 Cone: tmpOffset.set(0, 0, 0); @@ -1155,10 +1093,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; @@ -1167,7 +1105,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); @@ -1201,8 +1139,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) { @@ -1283,6 +1221,9 @@ class EmitterObject extends h3d.scene.Object { return; } + curScaleMultiplier = evaluator.getFloat(scaleMultiplier, curTime); + curOrbitSpeedMultiplier = evaluator.getFloat(orbitSpeedMultiplier, curTime); + if(subEmitters != null) { for( se in subEmitters ) { se.tick(dt); @@ -1299,7 +1240,7 @@ class EmitterObject extends h3d.scene.Object { return; if( parentNonEmitter != null ) { - if (particleScaling == None) { + if (prefab.particleScaling == None) { worldScale.set(1.0,1.0,1.0); } else { @@ -1309,15 +1250,15 @@ 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)); + 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 InfinityRandom: - // Every emitRateChangeDelay time, we choose a new emit rate value between min and max + // Every emitRateChangeTime 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). @@ -1326,9 +1267,9 @@ class EmitterObject extends h3d.scene.Object { var max = evaluator.getFloat(emitRateMax, curTime); var unInitiliazed = Math.isNaN(emitRateCurrent); - var needNewValue = curTime - emitRateChangeDelayStart >= emitRateChangeDelay || curTime - emitRateChangeDelayStart < 0; + var needNewValue = curTime - emitRateChangeStart >= prefab.emitRateChangeTime || curTime - emitRateChangeStart < 0; if (unInitiliazed || needNewValue) { - emitRateChangeDelayStart = curTime; + emitRateChangeStart = curTime; emitRatePrevious = emitRateTarget; emitRateTarget = random.rand() * (max-min) + min; @@ -1336,48 +1277,48 @@ class EmitterObject extends h3d.scene.Object { emitRatePrevious = random.rand() * (max-min) + min; } - var t = (curTime - emitRateChangeDelayStart) / emitRateChangeDelay; + var t = (curTime - emitRateChangeStart) / prefab.emitRateChangeTime; emitRateCurrent = (emitRatePrevious - emitRateTarget) * t + emitRatePrevious; emitTarget += emitRateCurrent * hxd.Math.clamp(emitScale) * dt; - var delta = hxd.Math.ceil(hxd.Math.min(maxCount - numInstances, emitTarget - emitCount)); + 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)); + emitTarget += evaluator.getFloat(emitRate, hxd.Math.min(curTime, prefab.emitDuration)) * hxd.Math.clamp(emitScale) * dt; + var delta = hxd.Math.ceil(hxd.Math.min(prefab.maxCount - numInstances, emitTarget - emitCount)); doEmit(delta); - if( isSubEmitter && curTime >= emitDuration ) + 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, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); 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, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); doEmit(delta); totalBurstCount++; } } - if( isSubEmitter && totalBurstCount == burstCount ) + if( isSubEmitter && totalBurstCount == prefab.burstCount ) enable = false; } @@ -1395,14 +1336,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); @@ -1413,14 +1354,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); @@ -1441,7 +1382,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 { @@ -1462,7 +1403,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); } @@ -1482,7 +1423,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) @@ -1507,7 +1448,7 @@ class EmitterObject extends h3d.scene.Object { batch.emitInstance(); } - switch(sortMode) { + switch(prefab.sortMode) { case Depth: depthSort(); var p = listHead; @@ -1531,7 +1472,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()); @@ -1539,12 +1480,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); @@ -1554,7 +1495,7 @@ class EmitterObject extends h3d.scene.Object { } // Particle space <-> emitter space, for orbit speed - if (instDef.orbitSpeed != VZero) { + if (partOrbitSpeed != VZero) { orbitToEmitter.multiply(parentTransform, getInvPos()); orbitFromEmitter.initInverse(orbitToEmitter); } @@ -1600,7 +1541,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 @@ -1633,7 +1574,7 @@ class EmitterObject extends h3d.scene.Object { } public function setRandSeed(seed: Int) { - randomSeed = seed ^ seedGroup; + randomSeed = seed ^ prefab.seed; reset(); } @@ -1644,7 +1585,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; @@ -1664,13 +1605,13 @@ 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) + if (prefab.burstInterval > 0) + totalBurstCount = hxd.Math.ceil(hxd.Math.max(0.0, localTime/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 (curTime > prefab.emitDuration) enable = false; } @@ -1682,7 +1623,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); @@ -1693,7 +1634,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; @@ -1714,8 +1655,8 @@ 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)); @@ -1725,7 +1666,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) { @@ -1750,162 +1691,159 @@ class EmitterObject extends h3d.scene.Object { @:prefabName("Emitter") @:prefabIcon(hrt.ui.HuiRes.ui.icons.prefab.emitter) -@:build(hrt.prefab.fx.EmitterMacros.build()) +@:build(hrt.prefab.fx.EmitterMacros.build("hrt.prefab.fx.Emitter.EmitterObject")) 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" }) + @: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: constant rate. Infinity Random: rate changing between min and max. Duration: 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" }) + @:s @:range(0, null) @:animate @:disp("Rate", "Number of particles emitted per second") public var emitRate : Float = 5; - @:param({ t: PInt(0, 100), disp: "Rate Min", animate: true, group: "Emit Params" }) + @:s @:range(0, null) @:animate @:disp("Rate Min", "Minimum number of particles emitted per second") public var emitRateMin : Float = 5; - @:param({ t: PInt(0, 100), disp: "Rate Max", animate: true, group: "Emit Params" }) + @:s @:range(0, null) @:animate @:disp("Rate Max", "Maximum number of particles emitted per second") 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" }) + @:s @:range(0.01, null) @:disp("Rate Change Time", "Infinity Random: 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" }) + @:s @:range(0, null) @:disp("Burst Interval", "Time between two bursts, in seconds") + public var burstInterval : Float = 1.0; + @:s @:range(1, null) @:animate @:disp("Particles Per Burst", "Number of particles emitted by each burst") public var burstParticleCount : Float = 1; - @:param({ t: PInt(0, 100), group: "Emit Params" }) + @: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" }) + @:group("Particle Movement") + @:param(PVec(3), Spawn) @:disp("Start Speed", "Initial speed in the emitter orientation, set at the spawn of the particle") + public var startSpeed : ParamVec3; + @:param(PVec(3), Spawn) @:disp("Start World Speed", "Initial speed in world space, set at the spawn of the particle") + public var startWorldSpeed : ParamVec3; + @:param(PVec(3), Particle) @:disp("Constant Speed", "Speed in the emitter orientation, added to the velocity without being accumulated") + public var partSpeed : ParamVec3; + @:param(PVec(3), Particle) @:disp("Constant 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; + + @:group("Color") + @:s @:disp("Random Color", "Multiplies the color of each particle by a random color, taken between two colors or in a gradient") + public var randomColorMode : RandomColorMode = RandomColorMode.None; + @:s @:disp("Color 1", "First bound of the random color") public var randomColor1 : Array = [0., 0., 0., 1.]; - @:param({ t: PVec(4), disp: "Color 2", group: "Color" }) + @:s @:disp("Color 2", "Second bound of the random color") public var randomColor2 : Array = [1., 1., 1., 1.]; - @:param({ t: PGradient, disp: "Gradient", group: "Color" }) + @:s @:disp("Gradient", "Gradient in which the random color is taken") public var randomGradient : GradientData; - // ANIMATION - @:param({ t: PFile(["jpg", "png"]), disp: "Sheet", group: "Sprite Sheet Animation" }) + @: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 @@ -1913,20 +1851,26 @@ 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( !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 ) + if( randomGradient == null && randomColorMode == RandomColorMode.Gradient ) randomGradient = Gradient.getDefaultGradientData(); } @@ -1951,7 +1895,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) && @@ -1960,67 +1903,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; + var params = makeValues(emitterObj); emitterObj.particleTemplate = template; // SUB-EMITTER @@ -2031,77 +1914,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(params.randomSlots, 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 } @@ -2119,10 +1940,10 @@ 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"]; - function getVisibleParams() : Array { - var params = emitterParams.copy(); + function getVisibleParams() : Array { + var params = PROPERTIES.copy(); inline function removeParam(pname: String) { params.remove(params.find(p -> p.name == pname)); } @@ -2134,7 +1955,7 @@ class Emitter extends Object3D { removeParam("emitRad2"); } if(emitShape != Dome) { - removeParam("emitDomeCut"); + removeParam("emitDomeHeight"); removeParam("emitDomeFlip"); } @@ -2154,34 +1975,26 @@ class Emitter extends Object3D { removeParam("killOnCollision"); } - if(!useRandomColor) { - removeParam("useRandomGradient"); + if(randomColorMode != RandomColorMode.Colors) { removeParam("randomColor1"); removeParam("randomColor2"); - removeParam("randomGradient"); - } - else { - if (useRandomGradient) { - removeParam("randomColor1"); - removeParam("randomColor2"); - } else { - removeParam("randomGradient"); - } } + if(randomColorMode != RandomColorMode.Gradient) + removeParam("randomGradient"); switch (emitType) { case Infinity: removeParam("burstCount"); - removeParam("burstDelay"); + removeParam("burstInterval"); removeParam("burstParticleCount"); removeParam("emitDuration"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); case InfinityRandom: removeParam("emitRate"); removeParam("burstCount"); - removeParam("burstDelay"); + removeParam("burstInterval"); removeParam("burstParticleCount"); removeParam("emitDuration"); case BurstDuration: @@ -2189,20 +2002,20 @@ class Emitter extends Object3D { removeParam("burstCount"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); case Burst: removeParam("emitDuration"); removeParam("emitRate"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); case Duration: removeParam("burstCount"); - removeParam("burstDelay"); + removeParam("burstInterval"); removeParam("burstParticleCount"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); } return params; } @@ -2213,8 +2026,8 @@ 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 ? 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"); @@ -2224,127 +2037,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, getVisibleParams(), 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) @@ -2382,7 +2079,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,7 +2153,7 @@ class Emitter extends Object3D { case Sphere: mesh = new h3d.scene.Sphere(0xffffff, 0.5, true, debugShape); 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..c0d1cf65b 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,40 @@ 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 (emitter.partOffset != VZero) { + evaluator.getVector2(idx, emitter.partOffset, 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 (emitter.partRotation != VZero) { + var r = evaluator.getFast(idx, emitter.partRotation, 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.getVector2(idx, emitter.partStretch, t, tmpVec); + tmpVec *= evaluator.getFast(idx, emitter.partScale, 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 startAngle = evaluator.getFloat(emitter.emitStartAngle, emitter.curTime) * hxd.Math.PI / 180; + var endAngle = evaluator.getFloat(emitter.emitEndAngle, emitter.curTime) * hxd.Math.PI / 180; // normalize the angular span to [0, 2PI) var twoPI = Math.PI * 2; @@ -125,11 +124,11 @@ 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); @@ -140,41 +139,41 @@ class Particle2DInstance extends h2d.SpriteBatch.BatchElement { 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); + tmpVec.set(evaluator.getSpawn(idx, emitter.startSpeed[0], emitter.curTime), evaluator.getSpawn(idx, emitter.startSpeed[1], emitter.curTime)); tmpVec.transform2x2(m); speed += tmpVec; // START WORLD SPEED - evaluator.getVector2(idx, emitter.startWorldSpeed, emitter.curTime, tmpVec); + tmpVec.set(evaluator.getSpawn(idx, emitter.startWorldSpeed[0], emitter.curTime), evaluator.getSpawn(idx, emitter.startWorldSpeed[1], emitter.curTime)); tmpVec.transform2x2(emitter.invTransform); speed += tmpVec; } // ACCELERATION - if (def.acceleration != VZero) { - evaluator.getVector2(idx, def.acceleration, t, tmpVec); + if (emitter.partAcceleration != VZero) { + evaluator.getVector2(idx, emitter.partAcceleration, t, tmpVec); tmpVec.scale(dt); tmpVec.transform2x2(m); speed += tmpVec; } // WORLD ACCELERATION - if (def.worldAcceleration != VZero) { - evaluator.getVector2(idx, def.worldAcceleration, t, tmpVec); + if (emitter.partWorldAcceleration != VZero) { + evaluator.getVector2(idx, emitter.partWorldAcceleration, t, tmpVec); tmpVec.scale(dt); tmpVec.transform2x2(emitter.invTransform); speed += tmpVec; @@ -183,30 +182,30 @@ 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 (emitter.partSpeed != VZero) { + evaluator.getVector2(idx, emitter.partSpeed, t, tmpVec); tmpVec.transform2x2(m); tmpSpeed += tmpVec; } // DAMPEN - if (def.dampen != VZero) { - var dampen = evaluator.getFloat(idx, def.dampen, t); + if (emitter.partDampen != VZero) { + var dampen = evaluator.getFast(idx, emitter.partDampen, t); var scale = Math.exp(dampen* -dt); speed.scale(scale); } // WORLD SPEED - if (def.worldSpeed != VZero) { - evaluator.getVector2(idx, def.worldSpeed, t, tmpVec); + if (emitter.partWorldSpeed != VZero) { + evaluator.getVector2(idx, emitter.partWorldSpeed, t, tmpVec); tmpVec.transform2x2(emitter.invTransform); tmpSpeed += tmpVec; } // MAX VELOCITY - if (def.maxVelocity != VZero) { - var maxVel = evaluator.getFloat(idx, def.maxVelocity, t); + if (emitter.partMaxSpeed != VZero) { + var maxVel = evaluator.getFast(idx, emitter.partMaxSpeed, t); var curVelSq = tmpSpeed.lengthSq(); if (maxVel * maxVel < curVelSq) { tmpSpeed.normalize(); @@ -216,11 +215,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 (emitter.partOrbitSpeed != VZero) { + var orbitSpeed = evaluator.getFast(idx, emitter.partOrbitSpeed, 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,7 +233,7 @@ 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); } @@ -249,58 +248,52 @@ class Emitter2DObject extends h2d.Object { 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 prefab : Emitter2D; + + // Emission public var emitRate : Value; public var emitRateMin : Value; public var emitRateMax : Value; - public var emitRateChangeDelay : Float = 1.0; - 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 burstParticleCount : Value; + public var lifeTime : SpawnValue; + public var emitScale : Float = 1.0; // multiplier of the emission, set by the game + + // Emit Shape public var emitRadius : Value; - public var emitAngle1 : Value; - public var emitAngle2 : Value; + public var emitStartAngle : Value; + public var emitEndAngle : 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; + + // Particle Movement + public var startSpeed : Array; + public var startWorldSpeed : Array; + public var partSpeed : Value; + public var partWorldSpeed : Value; + public var partAcceleration : Value; + public var partWorldAcceleration : Value; + public var partOrbitSpeed : Value; + public var orbitSpeedMultiplier : Value; + + // Limit Velocity + public var partDampen : Value; + public var partMaxSpeed : Value; + + // Particle Transform + public var partOffset : Value; + public var partRotation : Value; + public var partScale : Value; + public var scaleMultiplier : Value; + public var partStretch : Value; + public var invTransform = new h2d.col.Matrix(); public function new(?parent) { @@ -308,24 +301,26 @@ 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); + evaluator = new Evaluator(); + 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); + this.prefab = prefab; + evaluator.allocRandom(prefab.maxCount, randSlots); + 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(lifeTime); + 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 +328,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 +345,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) { @@ -359,10 +354,13 @@ class Emitter2DObject extends h2d.Object { } function tick(dt : Float) { - switch (emitType) { + curScaleMultiplier = evaluator.getFloat(scaleMultiplier, curTime); + curOrbitSpeedMultiplier = evaluator.getFloat(orbitSpeedMultiplier, 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); + countToEmit += hxd.Math.min(prefab.maxCount - particles.length, emitTarget); var t = hxd.Math.floor(countToEmit); countToEmit -= t; doEmit(t); @@ -372,24 +370,24 @@ class Emitter2DObject extends h2d.Object { case Duration: var emitTarget = evaluator.getFloat(emitRate, curTime) * hxd.Math.clamp(emitScale) * dt; - countToEmit += curTime >= emitDuration ? 0 : hxd.Math.min(maxCount - particles.length, emitTarget); + 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, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); 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, Std.int(evaluator.getFloat(burstParticleCount, curTime)))); doEmit(emitTarget); } } @@ -412,10 +410,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.getSpawn(part.idx, lifeTime, curTime); this.particles.push(part); - instanceCounter = (instanceCounter + 1) % maxCount; + instanceCounter = (instanceCounter + 1) % prefab.maxCount; } } @@ -426,29 +424,25 @@ 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); } 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 = h2d.Tile.fromColor(h3d.Vector.fromArray(prefab.color).toColor(), 10, 10); + case Colors: + var c1 = prefab.randomColor1, c2 = prefab.randomColor2, k = rand.rand(); + t = h2d.Tile.fromColor(new h3d.Vector(hxd.Math.lerp(c1[0], c2[0], k), hxd.Math.lerp(c1[1], c2[1], k), hxd.Math.lerp(c1[2], c2[2], k)).toColor(), 10, 10); + case Gradient: + t = h2d.Tile.fromColor(Gradient.evalData(prefab.randomGradient, rand.rand()).toColor(), 10, 10); } 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 +455,158 @@ class Emitter2DObject extends h2d.Object { @:access(hrt.prefab.fx.Emitter2DObject) @:prefabName("Emitter 2D") @:prefabIcon(hrt.ui.HuiRes.ui.icons.prefab.emitter2d) +@:build(hrt.prefab.fx.EmitterMacros.build("hrt.prefab.fx.Emitter2D.Emitter2DObject")) 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; - }; + @: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: constant rate. Infinity Random: rate changing between min and max. Duration: 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; + @:s @:range(0, 100) @:animate @:disp("Rate", "Number of particles emitted per second") + public var emitRate : Float = 5; + @:s @:range(0, 100) @:animate @:disp("Rate Min", "Minimum number of particles emitted per second") + public var emitRateMin : Float = 5; + @:s @:range(0, 100) @:animate @:disp("Rate Max", "Maximum number of particles emitted per second") + public var emitRateMax : Float = 5; + @:s @:range(0.01, 5.0) @:disp("Rate Change Time", "Infinity Random: 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; + @:s @:range(1, 10) @:animate @:disp("Particles Per Burst", "Number of particles emitted by each burst") + public var burstParticleCount : Float = 1; + @: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, set at the spawn of the particle") + public var startSpeed : ParamVec2; + @:param(PVec(2, -10, 10), Spawn) @:disp("Start World Speed", "Initial speed in world space, set at the spawn of the particle") + public var startWorldSpeed : ParamVec2; + @:param(PVec(2, -10, 10), Particle) @:disp("Constant Speed", "Speed in the emitter orientation, added to the velocity without being accumulated") + public var partSpeed : ParamVec2; + @:param(PVec(2, -10, 10), Particle) @:disp("Constant 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("Color") + @:s @:disp("Sprite Type", "Drawing of the particles: a plain color, a texture or a sprite sheet") + public var spriteType : SpriteType = SpriteType.Color; + @:s @:disp("Color", "Color of the particles") + public var color : Array = [0., 0., 0., 1.]; + @: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 @:disp("Color 1", "First bound of the random color") + public var randomColor1 : Array = [0., 0., 0., 1.]; + @:s @:disp("Color 2", "Second bound of the random color") + public var randomColor2 : Array = [1., 1., 1., 1.]; + @:s @: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 = 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,197 +617,38 @@ 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; + var params = makeValues(emitterObj); 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(params.randomSlots, 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(); + #if (editor || editor_hl) + function getVisibleParams() : Array { + var params = PROPERTIES.copy(); inline function removeParam(pname: String) { params.remove(params.find(p -> p.name == pname)); } - var emitShape : Emit2DShape = EmitterHelper.getParamVal(PARAMS, props, "emitShape"); if(!emitShape.match(Circle)) { removeParam("emitRadius"); - removeParam("emitAngle1"); - removeParam("emitAngle2"); + removeParam("emitStartAngle"); + removeParam("emitEndAngle"); } if(!emitShape.match(Rectangle)) { removeParam("emitWidth"); removeParam("emitHeight"); } - var spriteType = EmitterHelper.getParamVal(PARAMS, props, "spriteType"); if (spriteType != SpriteType.Color) { removeParam("color"); - removeParam("useRandomColor"); + removeParam("randomColorMode"); } if (spriteType != SpriteType.Texture) { removeParam("texture"); @@ -837,41 +658,32 @@ class Emitter2D extends Object2D { removeParam("frameCount"); removeParam("frameDivisionX"); removeParam("frameDivisionY"); - removeParam("animationSpeed"); + removeParam("animationCycles"); removeParam("animationLoop"); removeParam("animationUseSourceUVs"); removeParam("animationBlendBetweenFrames"); } - if(!EmitterHelper.getParamVal(PARAMS, props, "useRandomColor")) { - removeParam("useRandomGradient"); + if (spriteType != SpriteType.Color || randomColorMode != RandomColorMode.Colors) { 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.Gradient) + removeParam("randomGradient"); - var emitType : EmitType = EmitterHelper.getParamVal(PARAMS, props, "emitType"); switch (emitType) { case Infinity: removeParam("burstCount"); - removeParam("burstDelay"); + removeParam("burstInterval"); removeParam("burstParticleCount"); removeParam("emitDuration"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); case InfinityRandom: removeParam("emitRate"); removeParam("burstCount"); - removeParam("burstDelay"); + removeParam("burstInterval"); removeParam("burstParticleCount"); removeParam("emitDuration"); case BurstDuration: @@ -879,25 +691,45 @@ class Emitter2D extends Object2D { removeParam("burstCount"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); case Burst: removeParam("emitDuration"); removeParam("emitRate"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); case Duration: removeParam("burstCount"); - removeParam("burstDelay"); + removeParam("burstInterval"); removeParam("burstParticleCount"); removeParam("emitRateMin"); removeParam("emitRateMax"); - removeParam("emitRateChangeDelay"); + removeParam("emitRateChangeTime"); + } + + return params; + } + + 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 ? 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, getVisibleParams(), onChange); } + #if editor override function getHideProps() : hide.prefab.HideProps { return { icon : "asterisk", @@ -915,14 +747,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.emitStartAngle, 0) * hxd.Math.PI / 180; + var radA2 = e.evaluator.getFloat(e.emitEndAngle, 0) * hxd.Math.PI / 180; + graphics.drawPie(0, 0, e.evaluator.getFloat(e.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.emitWidth, 0); + var height = e.evaluator.getFloat(e.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..28ca21f33 100644 --- a/hrt/prefab/fx/EmitterHelper.hx +++ b/hrt/prefab/fx/EmitterHelper.hx @@ -1,68 +1,95 @@ 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 } 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); + + public static function isEmitter( p : Prefab ) { + return p is Emitter || p is Emitter2D; } - public static function randProp(name: String) { - return name + "_rand"; + public static function getProperties( p : Prefab ) : Array { + var e = Std.downcast(p, Emitter); + if( e != null ) return e.getProperties(); + var e2 = Std.downcast(p, Emitter2D); + if( e2 != null ) return e2.getProperties(); + return []; } - 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); + public static function getParams( p : Prefab ) : Array { + var e = Std.downcast(p, Emitter); + if( e != null ) return e.getParams(); + var e2 = Std.downcast(p, Emitter2D); + if( e2 != null ) return e2.getParams(); + return []; } - public static function toArray( v : Dynamic ) : Array { - if( v == null ) - return null; - var a : Array = v; - return [for( x in a ) (x : Float)]; + public static function getFieldsOrder( p : Prefab ) : Array { + var e = Std.downcast(p, Emitter); + if( e != null ) return e.getFieldsOrder(); + var e2 = Std.downcast(p, Emitter2D); + if( e2 != null ) return e2.getFieldsOrder(); + return []; } - 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 mode of the component `comp` of the param `name` of an emitter, or of a shader under an emitter: Curve when the curve of + that component (`EmitterParam.curveName`) is in its `particleParams`, else null + **/ + public static function getMode( p : Prefab, name : String, comp : Int ) : ParamMode { + var e = Std.downcast(p, Emitter); + if( e != null ) return e.getMode(name, comp); + var e2 = Std.downcast(p, Emitter2D); + if( e2 != null ) return e2.getMode(name, comp); + var shader = Std.downcast(p, hrt.prefab.Shader); + if( shader != null ) { + var pp = shader.particleParams; + return pp.contains(name + ":" + EmitterParam.COMPONENTS[comp]) || (comp == 0 && pp.contains(name)) ? Curve : null; + } + return null; } - 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; + /** For a shader, a Constant sets its prop, a Curve adds its particle curve **/ + public static function setMode( p : Prefab, name : String, comp : Int, m : ParamMode ) { + var e = Std.downcast(p, Emitter); + if( e != null ) { + e.setMode(name, comp, m); + return; } - 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); + var e2 = Std.downcast(p, Emitter2D); + if( e2 != null ) { + e2.setMode(name, comp, m); + return; + } + var shader = Std.downcast(p, hrt.prefab.Shader); + if( shader == null ) + throw "Not an emitter or a shader: " + p.getAbsPath(true); + var size = EmitterParam.getParamSize(Lambda.find(shader.getShaderDefinition().data.vars, v -> v.name == name)); + var key = EmitterParam.curveName(name, size, comp); + shader.particleParams.remove(key); + switch( m ) { + case Curve: + shader.particleParams.push(key); + shader.particleParams.sort(Reflect.compare); + 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, _: } - if(isVector) - return h3d.Vector.fromArray(val); - return val; } public static inline function rotateZ( m : h3d.Matrix, angle : Float ) { @@ -149,130 +176,417 @@ 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 + + // Build inspector for an emitter parameter + public 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 }; + } + var comps = [for (i in 0...size) i]; + var value = [for (i in comps) shownMode(prefab, def, i)]; - 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); + 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.stealLabel = false; + line.tooltip = def.desc; + 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.stealLabel = false; + line.tooltip = def.desc; + 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, ?field: String) { + var s = new hide.kit.Slider(line, sid); + if (size > 1) + menuOwners.push(s); + if (def.kind != Particle) + s.anim = EmitterParam.animKey(def, i, field); + 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; + } + + 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); + g.onValueChange = (_) -> ctx.rebuildPrefab(prefab); + if (onSpreadChange != null) + g.onSpreadChange = onSpreadChange; + if (size > 1) + menuOwners.push(g); + var edited = getEditableCurve(prefab, EmitterParam.animKey(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"; + } } - 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(); - }); + 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]; } - 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; - }); + case Range(min, max): + var minSlider = slider("min", "Min", min, "min"); + var maxSlider = slider("max", "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); } - else { - var btn = new hide.Element('').appendTo(dd); - btn.click(function(e) { - addUndo(p.name); - EmitterHelper.resetParam(props, p); - refresh(); - }); + + function apply() { + bound(); + setParamMode(prefab, def, i, Range(minSlider.value, maxSlider.value)); } - 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 minSlider.onFieldChange = (_) -> { + minSlider.value = Math.min(minSlider.value, maxSlider.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(); - }); + @:privateAccess maxSlider.onFieldChange = (_) -> { + maxSlider.value = Math.max(maxSlider.value, minSlider.value); + apply(); } + 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, properties: 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; + } + + var params = getParams(emitter); + for (name in getFieldsOrder(emitter)) { + var property = Lambda.find(properties, p -> p.name == name); + if (property != null) { + editProperty(getCategory(property.groupName ?? "Emitter"), emitter, property, changed); + continue; + } + var param = Lambda.find(params, p -> p.name == name); + if (param != null) + editParam(ctx, emitter, param, getAllowedModes(param.kind), getCategory(param.groupName)); + } + } + + static function editProperty(cat: hide.kit.Category, emitter: Prefab, p: PropertyDef, changed: (name: String, temp: Bool) -> Void) { + 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); + 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); } + default: + var widget = cat.buildProp(p, emitter); + if (widget == null) + return; + if (!p.animate) + widget.anim = null; + widget.onValueChange = (temp) -> changed(p.name, temp); + } + } + + static function getAllowedModes(kind: ParamKind) : Array { + return switch (kind) { + case Spawn: [Constant, Range]; + case Particle: [Constant, Range, Curve, RandomCurve, CurveRange]; + } + } + - 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 is shown as the default value: it is not saved (`EmitterParam.saved`) **/ + 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)); + } + + /** A mode set back to the default value of the param unsets it **/ + static function setParamMode(prefab: Prefab, def: ParamDef, i: Int, m: ParamMode) { + setMode(prefab, def.name, i, isDefault(def, i, m) ? null : m); + } + + /** The mode `mode` for the component `i` of `def`, from its current one: its curve (`EmitterParam.animKey`) gets the new values **/ + static function convert(prefab: Prefab, def: ParamDef, i: Int, mode: ModeKind) : ParamMode { + var base = def.def[i]; + var key = EmitterParam.animKey(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; + } + } + + /** The curve `name` of `owner` gets `datas` (two for a Range), made when it has none **/ + 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; + } + + /** The keys of the curve `src` remapped from 0..1 to `from`..`to`, or a line from `from` to `to` without `src` **/ + 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; + } + + /** The curves the params of `owner` read **/ + static function getModeCurves(owner: Prefab) : Array { + var names = []; + var shader = Std.downcast(owner, hrt.prefab.Shader); + if (shader != null) + names = shader.particleParams.copy(); + else + for (p in getParams(owner)) + for (i in 0...p.def.length) + if (EmitterParam.isCurveMode(getMode(owner, p.name, i))) + names.push(EmitterParam.animKey(p, i)); + 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; } + + /** Not the one of a trail or a sub-emitter it is under **/ + 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..0b2a05e7c 100644 --- a/hrt/prefab/fx/EmitterMacros.hx +++ b/hrt/prefab/fx/EmitterMacros.hx @@ -3,260 +3,191 @@ 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; + - `@:param(PVec(3, -10, 10), Particle, default)` params with modes (`EmitterParam`), saved under their name like the properties, which is + also the field of their value in the emitter object. The Particle ones, read over the particle life, are named `partX`; + - `@:group("Name")`: the inspector group of the field and of the next ones. + The inspector shows the properties and the params in their declaration order. + The random slots are allocated in the order of `makeValues`: particle params, properties, spawn params. +**/ 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); - } - } - return null; - } - - static function field( fl : Array, name : String ) : Null { - for( f in fl ) - if( f.field == name ) - return f.expr; + if( m.name == name ) + return m.params; 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 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; + 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); } } - public static function build() : Array { + public static function build( objectType : String ) : Array { var fields = Context.getBuildFields(); var pos = Context.currentPos(); - var params : Array = []; - var emitterDecls : Array = []; - var instDecls : Array = []; - var extra : Array = []; + var properties = [], params = [], order = []; + var load = [], save = [], copy = [], getCases = [], setCases = []; + var particleValues = [], particleApply = [], propertyValues = [], propertyApply = [], spawnValues = [], spawnApply = []; + 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; + order.push(n); + properties.push(macro @:pos(p) { name: $v{n}, t: ${propType(f, t, init)}, def: (${init ?? macro null} : Dynamic), groupName: $v{group}, + animate: $v{animate}, 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 ) { + propertyApply.push(macro @:pos(p) obj.$n = v.properties[$v{propertyValues.length}]); + propertyValues.push(macro @:pos(p) v.properties.push(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; + order.push(n); + 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 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; + var def = macro @:pos(p) PARAMS[$v{params.length}]; + 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 }); + } else { + 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(hrt.prefab.fx.EmitterParam.savedVec(this, $def, this.$n)); if( v != null ) data.$n = v; }); + getCases.push({ values: [macro $v{n}], expr: macro hrt.prefab.fx.EmitterParam.comp(this.$n, comp) }); + setCases.push({ values: [macro $v{n}], expr: macro this.$n = hrt.prefab.fx.EmitterParam.withComp(this.$n, comp, m) }); } - switch( p.kind ) { - case KEnum(_): set = macro @:pos(pos) Reflect.setField(props, $v{n}, Type.enumConstructor(this.$n)); - default: + copy.push(macro @:pos(p) this.$n = from.$n); + if( kind == "Spawn" ) { + var i = spawnValues.length; + spawnApply.push(dim == 1 ? macro @:pos(p) obj.$n = v.spawn[$v{i}][0] : macro @:pos(p) obj.$n = v.spawn[$v{i}]); + spawnValues.push(macro @:pos(p) v.spawn.push(b.spawn($def))); + } else { + particleApply.push(macro @:pos(p) obj.$n = v.particle[$v{particleValues.length}]); + particleValues.push(macro @:pos(p) v.particle.push(b.param($def))); } - 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; - }); } + // 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; + } + return sharedValues; + } : macro return buildValues(); + var objCT = Context.toComplexType(Context.getType(objectType)); 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 statics first, the ones of the class can use them + var statics = (macro class { + public static var PROPERTIES : Array = $a{properties}; + public static var PARAMS : Array = $a{params}; + public static var FIELDS_ORDER : Array = $v{order}; + }).fields; + var methods = (macro class { + function loadParams( data : Dynamic ) { + $reset; + $b{load}; + } + function saveParams( data : Dynamic ) { + $b{save}; + } + function copyParams( from : $selfType ) { + $b{copy}; + $copyValues; + } + public function getProperties() return PROPERTIES; + public function getParams() return PARAMS; + public function getFieldsOrder() return FIELDS_ORDER; + 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() : hrt.prefab.fx.EmitterParam.EmitterValues { + var b = new hrt.prefab.fx.EmitterParam.ParamBuilder(this); + var v = new hrt.prefab.fx.EmitterParam.EmitterValues(); + $b{particleValues}; + $b{propertyValues}; + $b{spawnValues}; + v.randomSlots = b.randomSlots; + return v; + } + public function getValues() : hrt.prefab.fx.EmitterParam.EmitterValues { + $getValues; + } + public function makeValues( obj : $objCT ) : hrt.prefab.fx.EmitterParam.EmitterValues { + var v = getValues(); + $b{particleApply}; + $b{propertyApply}; + $b{spawnApply}; + return v; + } + }).fields; + if( share ) { + methods.push({ name: "sharedValues", access: [], kind: FVar(macro : hrt.prefab.fx.EmitterParam.EmitterValues), pos: pos }); + methods.push({ name: "valuesFrom", access: [], kind: FVar(selfType), pos: pos }); + } + return statics.concat(fields).concat(methods); } #end } diff --git a/hrt/prefab/fx/EmitterParam.hx b/hrt/prefab/fx/EmitterParam.hx new file mode 100644 index 000000000..c59382927 --- /dev/null +++ b/hrt/prefab/fx/EmitterParam.hx @@ -0,0 +1,281 @@ +package hrt.prefab.fx; + +import hrt.prefab.fx.Value; + +enum ParamKind { + Spawn; + Particle; +} + +enum ParamMode { + Constant( value : Float ); + Range( min : Float, max : Float ); + /** The curve named after the param (`EmitterParam.animKey`), like all the curves of an emitter **/ + Curve; + RandomCurve( spreadMin : Float, spreadMax : Float ); + CurveRange; +} + +typedef ParamVec2 = { ?x : ParamMode, ?y : ParamMode }; +typedef ParamVec3 = { > ParamVec2, ?z : ParamMode }; + +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; +} + +class EmitterParam { + + public static var COMPONENTS = ["x", "y", "z", "w"]; + + /** The name of the curve of the component `comp` of a param (`name`, `name:x`), of one of its bounds with `field` (`name:x:min`) **/ + public static function animKey( def : ParamDef, comp : Int, ?field : String ) { + return curveName(def.name, def.def.length, comp, field); + } + + public static function curveName( name : String, size : Int, comp : Int, ?field : String ) { + var k = size == 1 ? name : name + ":" + COMPONENTS[comp]; + return field == null ? k : k + ":" + field; + } + + /** The animation curve names of the property `prop` of `prefab` (the min and max of a Range), null if it is not an emitter or not animatable **/ + public static function animKeys( prefab : Prefab, prop : String ) : Array { + if( !EmitterHelper.isEmitter(prefab) ) + return null; + var parts = prop.split(":"); + var def = Lambda.find(EmitterHelper.getParams(prefab), p -> p.name == parts[0]); + if( def == null ) + return Lambda.exists(EmitterHelper.getProperties(prefab), p -> p.animate == true && p.name == prop) ? [prop] : null; + var i = parts.length > 1 ? COMPONENTS.indexOf(parts[1]) : 0; + if( def.kind == Particle || i < 0 ) + return null; + return switch( EmitterHelper.getMode(prefab, def.name, i) ) { + case Range(_): [animKey(def, i, "min"), animKey(def, i, "max")]; + case null, _: [animKey(def, i)]; + } + } + + public static function isCurveMode( m : ParamMode ) { + return switch( m ) { + case Curve, RandomCurve(_), CurveRange: true; + case null, _: false; + } + } + + /** A curve named `name` under `owner`, disabled or not **/ + public 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()); + } + + /** The mode of the component `i` of `def` as it is saved: a curve mode without its curve (deleted) is not **/ + public static function saved( owner : Prefab, def : ParamDef, i : Int, m : ParamMode ) : ParamMode { + return isCurveMode(m) && !hasCurve(owner, animKey(def, i)) ? null : m; + } + + public static function savedVec( owner : Prefab, def : ParamDef, v : Dynamic ) : ParamVec3 { + var out : ParamVec3 = { x: saved(owner, def, 0, comp(v, 0)), y: saved(owner, def, 1, comp(v, 1)), z: def.def.length > 2 ? saved(owner, def, 2, comp(v, 2)) : null }; + return out.x == null && out.y == null && out.z == null ? null : out; + } + + public static function parseVec( v : Dynamic ) : ParamVec3 { + if( v == null ) + return null; + var out : ParamVec3 = { x: parseMode(v.x), y: parseMode(v.y), z: parseMode(v.z) }; + return out.x == null && out.y == null && out.z == null ? null : out; + } + + public static function saveVec( v : Dynamic ) : Dynamic { + if( v == null || (v.x == null && v.y == null && v.z == null) ) + return null; + var o : Dynamic = {}; + if( v.x != null ) o.x = saveMode(v.x); + if( v.y != null ) o.y = saveMode(v.y); + if( v.z != null ) o.z = saveMode(v.z); + return o; + } + + public static function comp( v : Dynamic, i : Int ) : ParamMode { + return v == null ? null : i == 0 ? v.x : i == 1 ? v.y : v.z; + } + + public static function withComp( v : Dynamic, i : Int, m : ParamMode ) : ParamVec3 { + var out : ParamVec3 = { x: i == 0 ? m : comp(v, 0), y: i == 1 ? m : comp(v, 1), z: i == 2 ? m : comp(v, 2) }; + return out.x == null && out.y == null && out.z == null ? null : out; + } + + 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" }; + } + } + + /** + The random spread of a RandomCurve as an affine function of the value `v` of its curve, `spread + slope * v`: `spreadMin` at the + lowest value of the curve, `spreadMax` at the highest, `spreadMin` for a flat curve. + **/ + 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 }; + } + + /** The range of the values of a param curve: both of its curves for a blend **/ + 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; + } +} + +/** The values `makeValues` gives an emitter object, never modified: out of the editor, the clones of an emitter share the ones of their source **/ +class EmitterValues { + public var particle : Array = []; + public var properties : Array = []; + public var spawn : Array> = []; + public var randomSlots : Int = 0; + public function new() { + } +} + +class ParamBuilder { + + var owner : Prefab; + public var randomSlots(default, null) = 0; + + public function new( owner : Prefab ) { + this.owner = owner; + } + + /** The curve `name` of `owner`, `def` without it **/ + public static function curve( owner : Prefab, name : String, def : Float ) : Value { + return hrt.prefab.fx.Curve.getCurve(owner, name)?.makeVal() ?? Evaluator.vVal(def); + } + + public 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"; + } + } + + /** A particle param component in mode `m`, `name` its curve **/ + public function comp( m : ParamMode, name : String, def : Float ) : Value { + return switch( m ) { + case Range(min, max): VRandom(randomSlots++, min, max); + case RandomCurve(spreadMin, spreadMax): + var rid = randomSlots++; + 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 = hrt.prefab.fx.Curve.getCurve(owner, name); + if( c == null ) + Evaluator.vVal(def); + else if( c.blendMode != hrt.prefab.fx.Curve.CurveBlendMode.Range ) + throw owner.getAbsPath(true) + ': a Curve Range needs a curve in Range mode'; + else + VCurveRange(randomSlots++, 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 EmitterHelper.getMode(owner, def.name, i); + } + + function component( def : ParamDef, i : Int ) : Value { + var m = mode(def, i), key = EmitterParam.animKey(def, i); + return def.kind == Particle ? comp(m, key, def.def[i]) : animated(key, fixedValue(owner, m, key, def.def[i])); + } + + public function spawn( def : ParamDef ) : Array { + return [for( i in 0...def.def.length ) { + var m = mode(def, i); + var s : SpawnValue = switch( m ) { + case Range(min, max): + function bound( field : String, v : Float ) { + return hrt.prefab.fx.Curve.getCurve(owner, EmitterParam.animKey(def, i, field))?.makeVal() ?? VConst(v); + } + { rid: randomSlots++, min: bound("min", min), max: bound("max", max) }; + case null, _: + var key = EmitterParam.animKey(def, i); + var v = animated(key, fixedValue(owner, m, key, def.def[i])); + { rid: -1, min: v, max: v }; + } + s; + }]; + } + + public function param( def : ParamDef ) : Value { + 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..6e7265f4c 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,85 @@ class Evaluator { } } - public function getFast(pidx: Int, val: Value, time: Float) : Float { + public function getMean(s: SpawnValue) : Float { + return s.rid < 0 ? getFloat(s.min, 0) : (getFloat(s.min, 0) + getFloat(s.max, 0)) * 0.5; + } + + public function getSpawn(pidx: Int, s: SpawnValue, time: Float) : Float { + var v = getFast(pidx, s.min, time); + return s.rid < 0 ? v : hxd.Math.lerp(v, getFast(pidx, s.max, time), getRandom(pidx, s.rid)); + } + + 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 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, getRandom(pidx, 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) * (getRandom(pidx, rid) * 2 - 1); + case VRandomParamCurve(rid, pc, spread, slope): + var v = getParamCurve(pc, time); + v + (spread + slope * v) * (getRandom(pidx, rid) * 2 - 1); + case VCurveRange(rid, a, b): hxd.Math.lerp(a.getVal(time), b.getVal(time), getRandom(pidx, 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; } + public inline function getSpawnVec(pidx: Int, s: Array, time: Float, vec: h3d.Vector4) { + vec.set(getSpawn(pidx, s[0], time), getSpawn(pidx, s[1], time), getSpawn(pidx, s[2], time), 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 +130,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 59e445c7b..b3e242617 100644 --- a/hrt/prefab/fx/FX.hx +++ b/hrt/prefab/fx/FX.hx @@ -1,9 +1,9 @@ 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; +import hrt.prefab.fx.BaseFX.BaseFXTools; import hrt.prefab.fx.Event; /** @@ -124,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)); @@ -568,86 +568,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)); } } @@ -658,33 +637,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); } } } @@ -742,7 +721,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 { @@ -755,31 +745,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) ); @@ -794,20 +769,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) { @@ -923,10 +892,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]; @@ -975,7 +990,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; @@ -1071,11 +1086,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/RandomColorMode.hx b/hrt/prefab/fx/RandomColorMode.hx new file mode 100644 index 000000000..6b7183f98 --- /dev/null +++ b/hrt/prefab/fx/RandomColorMode.hx @@ -0,0 +1,8 @@ +package hrt.prefab.fx; + +/** The random color of the particles of an emitter: none, between two colors, or in a gradient **/ +enum RandomColorMode { + None; + Colors; + Gradient; +} diff --git a/hrt/prefab/fx/Value.hx b/hrt/prefab/fx/Value.hx index 6a507a798..742203b3a 100644 --- a/hrt/prefab/fx/Value.hx +++ b/hrt/prefab/fx/Value.hx @@ -1,31 +1,36 @@ package hrt.prefab.fx; +/** Parameters that have a one-shot random at time of emission **/ +@:structInit class SpawnValue { + public var rid : Int; + public var min : Value; // TODO + public var max : Value; +} + +@: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/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)