diff --git a/src/edit-ops.ts b/src/edit-ops.ts index 4e9d912..62c053b 100644 --- a/src/edit-ops.ts +++ b/src/edit-ops.ts @@ -192,6 +192,40 @@ class ResetOp extends StateOp { } } +interface EntityColorAdjustment { + brightness: number + temperature: number + tint: number +} + +class EntityColorAdjustmentOp { + name = 'entityColorAdjustment'; + + splat: Splat; + oldAdj: EntityColorAdjustment; + newAdj: EntityColorAdjustment; + + constructor(options: { splat: Splat, oldAdj: EntityColorAdjustment, newAdj: EntityColorAdjustment }) { + this.splat = options.splat; + this.oldAdj = options.oldAdj; + this.newAdj = options.newAdj; + } + + do() { + this.splat.colorAdjustment = this.newAdj; + } + + undo() { + this.splat.colorAdjustment = this.oldAdj; + } + + destroy() { + this.splat = null; + this.oldAdj = null; + this.oldAdj = null; + } +} + // op for modifying a splat transform class EntityTransformOp { name = 'entityTransform'; @@ -348,5 +382,7 @@ export { EntityTransformOp, SplatsTransformOp, PlacePivotOp, - MultiOp + MultiOp, + EntityColorAdjustment, + EntityColorAdjustmentOp }; diff --git a/src/editor.ts b/src/editor.ts index bfa5923..8be8161 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -71,6 +71,10 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S scene.forceRender = true; }); + events.on('splat.color', () => { + scene.forceRender = true; + }); + events.on('camera.bound', () => { scene.forceRender = true; }); diff --git a/src/shaders/splat-shader.ts b/src/shaders/splat-shader.ts index 188af02..216e301 100644 --- a/src/shaders/splat-shader.ts +++ b/src/shaders/splat-shader.ts @@ -7,6 +7,8 @@ uniform sampler2D splatState; uniform highp usampler2D splatTransform; // per-splat index into transform palette uniform sampler2D transformPalette; // palette of transform matrices +uniform vec4 colorAdjustment; // rgba factors to be applied to SH0 + varying mediump vec2 texCoord; varying mediump vec4 color; flat varying highp uint vertexState; @@ -66,7 +68,7 @@ void main(void) vec4 v1v2 = calcV1V2(splat_cam.xyz, covA, covB, transpose(mat3(model_view))); // get color - color = texelFetch(splatColor, splatUV, 0); + color = texelFetch(splatColor, splatUV, 0) * colorAdjustment; // calculate scale based on alpha // float scale = min(1.0, sqrt(-log(1.0 / 255.0 / color.a)) / 2.0); diff --git a/src/splat.ts b/src/splat.ts index 3e54be3..f59e846 100644 --- a/src/splat.ts +++ b/src/splat.ts @@ -19,6 +19,7 @@ import { Serializer } from "./serializer"; import { State } from './splat-state'; import { vertexShader, fragmentShader } from './shaders/splat-shader'; import { TransformPalette } from './transform-palette'; +import { EntityColorAdjustment } from './edit-ops'; const vec = new Vec3(); const veca = new Vec3(); @@ -55,6 +56,11 @@ class Splat extends Element { localBoundDirty = true; worldBoundDirty = true; _visible = true; + _colorAdjustment: EntityColorAdjustment = { + brightness: 1, + temperature: 0, + tint: 0 + }; transformPalette: TransformPalette; selectionAlpha = 1; @@ -120,10 +126,18 @@ class Splat extends Element { // @ts-ignore instance.createMaterial(getMaterialOptions(instance.splat.hasSH ? bands : 0)); - const material = instance.material; + const material = instance.material; + const {brightness, temperature, tint} = this._colorAdjustment; + material.setParameter('splatState', this.stateTexture); material.setParameter('splatTransform', this.transformTexture); material.setParameter('transformPalette', this.transformPalette.texture); + material.setParameter('colorAdjustment', [ + (1.0 + temperature + tint) * brightness, + (1.0 - Math.abs(temperature / 2) - tint) * brightness, + (1.0 - temperature + tint / 2) * brightness, + 1 + ]); material.update(); }; @@ -391,6 +405,16 @@ class Splat extends Element { this.scene.events.fire('splat.visibility', this); } } + + set colorAdjustment(adj: EntityColorAdjustment){ + this._colorAdjustment = adj; + this.rebuildMaterial(this.scene.events.invoke('view.bands')); + this.scene.events.fire('splat.color', this); + } + + get colorAdjustment() { + return this._colorAdjustment; + } } export { Splat }; diff --git a/src/ui/color-panel.scss b/src/ui/color-panel.scss new file mode 100644 index 0000000..759fd1c --- /dev/null +++ b/src/ui/color-panel.scss @@ -0,0 +1,62 @@ + +#color-panel { + display: flex; + flex-direction: column; + + background-color: $bcg-primary; + + padding: 0px 6px 12px 6px; +} + +.color-row{ + height: 32px; + line-height: 32px; + width: 100%; + display: flex; + flex-direction: row; + flex-grow: 1; + align-items: center; +} + +.color-label{ + width: 70px; + flex-shrink: 0; + flex-grow: 0; + margin: 0px; +} + +.color-expand { + flex-grow: 1; +} + +$height: 22px; + +#color-panel > div > div.pcui-vector-input { + margin: 0px; + gap: 10px; + height: $height; +} + +#color-panel > div > div.pcui-numeric-input { + margin: 0px; + height: $height; + line-height: $height; + + & > input { + padding: 0px; + margin: 0px 0px 0px 4px; + height: $height; + } +} + +#color-panel > div > div > div.pcui-numeric-input { + margin: 0px; + height: $height; + line-height: $height; + + & > input { + padding: 0px; + margin: 0px 0px 0px 4px; + height: $height; + } +} \ No newline at end of file diff --git a/src/ui/color-panel.ts b/src/ui/color-panel.ts new file mode 100644 index 0000000..b0340e4 --- /dev/null +++ b/src/ui/color-panel.ts @@ -0,0 +1,162 @@ +import { Container, ContainerArgs, Label, NumericInput } from 'pcui'; +import { Events } from '../events'; +import { Splat } from '../splat'; +import { EntityColorAdjustmentOp } from '../edit-ops'; +import { localize } from './localization'; + +class ColorPanel extends Container { + constructor(events: Events, args: ContainerArgs = {}) { + args = { + ...args, + id: 'color-panel' + }; + + super(args); + + const brightness = new Container({ + class: 'color-row' + }); + + const brightnessLabel = new Label({ + class: 'color-label', + text: localize('color.brightness') + }); + + const brightnessInput = new NumericInput({ + class: 'color-expand', + precision: 2, + value: 1.0, + min: 0.0, + max: 10.0, + enabled: false + }); + + brightness.append(brightnessLabel); + brightness.append(brightnessInput); + + const temperature = new Container({ + class: 'color-row' + }); + + const temperatureLabel = new Label({ + class: 'color-label', + text: localize('color.temperature') + }); + + const temperatureInput = new NumericInput({ + class: 'color-expand', + precision: 2, + value: 0, + min: -0.5, + max: 0.5, + enabled: false + }); + + temperature.append(temperatureLabel); + temperature.append(temperatureInput); + + const tint = new Container({ + class: 'color-row' + }); + + const tintLabel = new Label({ + class: 'color-label', + text: localize('color.tint') + }); + + const tintInput = new NumericInput({ + class: 'color-expand', + precision: 2, + value: 0, + max: 0.5, + min: -0.5, + enabled: false + }); + + tint.append(tintLabel); + tint.append(tintInput); + + this.append(brightness); + this.append(temperature); + this.append(tint); + + let selection: Splat | null = null; + + let uiUpdating = false; + + const updateUI = () => { + uiUpdating = true; + brightnessInput.value = selection.colorAdjustment.brightness; + temperatureInput.value = selection.colorAdjustment.temperature; + tintInput.value = selection.colorAdjustment.tint; + uiUpdating = false; + }; + + events.on('selection.changed', (splat) => { + selection = splat; + + if (selection) { + // enable inputs + updateUI(); + temperatureInput.enabled = tintInput.enabled = brightnessInput.enabled = true; + } else { + // disable inputs + temperatureInput.enabled = tintInput.enabled = brightnessInput.enabled = false; + } + }); + + let op: EntityColorAdjustmentOp | null = null; + + const createOp = () => { + op = new EntityColorAdjustmentOp({ + splat: selection, + oldAdj: selection.colorAdjustment, + newAdj: selection.colorAdjustment + }); + }; + + const updateOp = () => { + op.newAdj = { + brightness: brightnessInput.value, + temperature: temperatureInput.value, + tint: tintInput.value + }; + + op.do(); + }; + + const submitOp = () => { + events.fire('edit.add', op); + op = null; + }; + + const change = () => { + if (!uiUpdating) { + if (op) { + updateOp(); + } else { + createOp(); + updateOp(); + submitOp(); + } + } + }; + + const mousedown = () => { + createOp(); + }; + + const mouseup = () => { + updateOp(); + submitOp(); + }; + + [brightnessInput, temperatureInput, tintInput].forEach((input) => { + input.on('change', change); + input.on('slider:mousedown', mousedown); + input.on('slider:mouseup', mouseup); + }); + } +} + +export { ColorPanel }; diff --git a/src/ui/localization.ts b/src/ui/localization.ts index 9292263..857e484 100644 --- a/src/ui/localization.ts +++ b/src/ui/localization.ts @@ -194,6 +194,10 @@ const localizeInit = () => { "position": "Position", "rotation": "Rotation", "scale": "Scale", + "color": "COLOR", + "color.brightness": "Brightness", + "color.temperature": "Temperature", + "color.tint": "Tint", // Options panel "options": "VIEW OPTIONS", diff --git a/src/ui/scene-panel.ts b/src/ui/scene-panel.ts index 94d87e4..870bf7e 100644 --- a/src/ui/scene-panel.ts +++ b/src/ui/scene-panel.ts @@ -7,6 +7,7 @@ import { localize } from './localization'; import sceneImportSvg from '../svg/import.svg'; import sceneNewSvg from '../svg/new.svg'; +import { ColorPanel } from './color-panel'; const createSvg = (svgString: string) => { const decodedStr = decodeURIComponent(svgString.substring('data:image/svg+xml,'.length)); @@ -92,10 +93,29 @@ class ScenePanel extends Container { transformHeader.append(transformIcon); transformHeader.append(transformLabel); + const colorHeader = new Container({ + class: `panel-header` + }); + + const colorIcon = new Label({ + text: '\uE111', + class: `panel-header-icon` + }); + + const colorLabel = new Label({ + text: localize('color'), + class: `panel-header-label` + }); + + colorHeader.append(colorIcon); + colorHeader.append(colorLabel); + this.append(sceneHeader); this.append(splatListContainer); this.append(transformHeader); this.append(new Transform(events)); + this.append(colorHeader); + this.append(new ColorPanel(events)); this.append(new Element({ class: `panel-header`, height: 20