diff --git a/Kha/Backends/HTML5/kha/js/vr/VrInterface.hx b/Kha/Backends/HTML5/kha/js/vr/VrInterface.hx
index 7edae3e..887bcf8 100755
--- a/Kha/Backends/HTML5/kha/js/vr/VrInterface.hx
+++ b/Kha/Backends/HTML5/kha/js/vr/VrInterface.hx
@@ -668,7 +668,7 @@ class VrInterface extends kha.vr.VrInterface {
public override function IsPresenting(): Bool {
var presenting = false;
- if (vrDisplay != null)
+ if (vrDisplay != null){
presenting = vrDisplay.isPresenting;
}
return presenting;
diff --git a/leenkx/Sources/leenkx/trait/CameraControls.hx b/leenkx/Sources/leenkx/trait/CameraControls.hx
new file mode 100644
index 0000000..e4f0004
--- /dev/null
+++ b/leenkx/Sources/leenkx/trait/CameraControls.hx
@@ -0,0 +1,338 @@
+package leenkx.trait;
+
+import iron.Trait;
+import iron.Scene;
+import iron.object.CameraObject;
+import iron.system.Input;
+import kha.graphics2.Graphics;
+import zui.Zui;
+import zui.Id;
+import iron.data.Data;
+import leenkx.ui.Canvas;
+import leenkx.renderpath.Postprocess;
+
+/**
+ * Camera Controls Trait for parameter controls via ZUI
+ */
+
+class CameraControls extends Trait {
+
+ @prop var enableUI: Bool = true;
+ @prop var toggleKey: String = "c";
+ @prop var defaultFStop: Float = 5.6;
+ @prop var defaultShutterSpeed: Float = 60.0;
+ @prop var defaultISO: Float = 400.0;
+ @prop var defaultFOV: Float = 50.0;
+ @prop var defaultAutoExposure: Bool = false;
+ @prop var defaultAutoFocus: Bool = false;
+ @prop var defaultFocusDistance: Float = 2.5;
+ @prop var exposureCompensation: Float = 8.0;
+
+ var ui: Zui;
+ var controlsVisible: Bool = false;
+ var camera: CameraObject;
+
+ var fStop: Float = 5.6;
+ var shutterSpeed: Float = 60.0;
+ var iso: Float = 400.0;
+ var fov: Float = 50.0;
+ var autoExposure: Bool = false;
+ var autoFocus: Bool = false;
+ var focusDistance: Float = 2.5;
+ var expComp: Float = 8.0;
+
+ var sensorSize: Float = 0.3;
+ var cocSize: Float = 0.03;
+
+ public function new() {
+ super();
+ notifyOnInit(init);
+ notifyOnUpdate(update);
+ notifyOnRender2D(render);
+ }
+
+ function init() {
+ camera = Scene.active.camera;
+ if (camera == null) {
+ trace("ERROR: No active camera available for camera control");
+ return;
+ }
+ fStop = defaultFStop;
+ shutterSpeed = defaultShutterSpeed;
+ iso = defaultISO;
+ fov = defaultFOV;
+ autoExposure = defaultAutoExposure;
+ autoFocus = defaultAutoFocus;
+ focusDistance = defaultFocusDistance;
+ expComp = exposureCompensation;
+ applyToRenderPipeline();
+ }
+
+ function update() {
+ if (!enableUI) return;
+ if (Input.getKeyboard().started(toggleKey)) {
+ controlsVisible = !controlsVisible;
+ if (controlsVisible && ui == null) {
+ Data.getFont(Canvas.defaultFontName, function(font: kha.Font) {
+ ui = new Zui({font: font});
+ var cam = iron.Scene.active.camera;
+ });
+ }
+ }
+
+ Input.occupied = controlsVisible;
+ }
+
+ function render(g: Graphics) {
+ if (!controlsVisible || ui == null) return;
+
+ g.end();
+ ui.begin(g);
+
+ drawControlPanel();
+
+ ui.end();
+ g.begin(false);
+ }
+
+ function drawControlPanel() {
+ var h = Id.handle();
+ h.nest(8000);
+ h.redraws = 1;
+
+ var panelWidth = 400;
+ var panelHeight = 600;
+ var panelX = 20;
+ var panelY = Std.int((iron.App.h() - panelHeight) / 2);
+
+ if (ui.window(h, panelX, panelY, panelWidth, panelHeight, true)) {
+ ui.text("CAMERA CONTROLS");
+ ui.separator();
+
+ if (camera == null) {
+ ui.text("ERROR: No active camera");
+ return;
+ }
+
+ ui.text("Camera: " + camera.name);
+ ui.separator();
+
+ ui.text("Requires Postprocess enabled");
+ ui.text("RP Blender settings");
+ ui.separator();
+
+ ui.text("F-Stop (Aperture)");
+ var hFStop = Id.handle();
+ hFStop.nest(8001);
+ hFStop.value = fStop;
+ fStop = ui.slider(hFStop, "f/" + formatValue(fStop, 1), 1.4, 16.0, true, 100, true, Left, true);
+
+ if (hFStop.changed) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ ui.text("Shutter Speed (Exposure)");
+ var hShutter = Id.handle();
+ hShutter.nest(8002);
+ hShutter.value = shutterSpeed;
+ shutterSpeed = ui.slider(hShutter, "1/" + Std.int(shutterSpeed) + "s", 15.0, 2000.0, true, 100, true, Left, true);
+
+ if (hShutter.changed) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ ui.text("ISO (Film Sensitivity)");
+ var hISO = Id.handle();
+ hISO.nest(8003);
+ hISO.value = iso;
+ iso = ui.slider(hISO, "ISO " + Std.int(iso), 100.0, 3200.0, true, 100, true, Left, true);
+
+ if (hISO.changed) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ ui.text("Field of View");
+ var hFOV = Id.handle();
+ hFOV.nest(8004);
+ hFOV.value = fov;
+ var focalLengthEquiv = fovToFocalLength(fov);
+ fov = ui.slider(hFOV, Std.int(focalLengthEquiv) + "mm", 10.0, 120.0, true, 100, true, Left, true);
+
+ if (hFOV.changed) {
+ applyFOVToCamera();
+ }
+
+ ui.separator();
+
+ var hAutoFocus = Id.handle();
+ hAutoFocus.nest(8012);
+ hAutoFocus.selected = autoFocus;
+
+ ui.row([1/5, 4/5]);
+ autoFocus = ui.check(hAutoFocus, "");
+ ui.text("Auto-Focus (GPU)");
+
+ if (hAutoFocus.changed) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ if (!autoFocus) {
+ ui.text("Focus Distance" + (Input.getKeyboard().down("shift") ? " (PRECISE)" : ""));
+ } else {
+ ui.text("Focus Distance (AUTO - disabled)");
+ }
+ var hFocus = Id.handle();
+ hFocus.nest(8005);
+ hFocus.value = focusDistance;
+
+ var basePrecision = (focusDistance < 2.0) ? 3000 : 1000;
+ var shiftMultiplier = Input.getKeyboard().down("shift") ? 4.0 : 1.0;
+ var focusPrecision = Std.int(basePrecision * shiftMultiplier);
+
+ if (!autoFocus) {
+ focusDistance = ui.slider(hFocus, formatValue(focusDistance, 4) + "m", 0.001, 5.000, true, focusPrecision, true, Left, true);
+ } else {
+ ui.slider(hFocus, formatValue(focusDistance, 4) + "m (AUTO)", 0.001, 5.000, true, focusPrecision, true, Left, false);
+ }
+
+ if (hFocus.changed && !autoFocus) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ ui.text("Sensor Size (DoF)");
+ var hSensor = Id.handle();
+ hSensor.nest(8010);
+ hSensor.value = sensorSize;
+ sensorSize = ui.slider(hSensor, formatValue(sensorSize, 1) + "mm", 10.0, 50.0, true, 100, true, Left, true);
+
+ if (hSensor.changed) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ ui.text("CoC (DoF Sharpness)");
+ var hCoC = Id.handle();
+ hCoC.nest(8011);
+ hCoC.value = cocSize;
+ cocSize = ui.slider(hCoC, formatValue(cocSize, 3) + "mm", 0.010, 0.100, true, 100, true, Left, true);
+
+ if (hCoC.changed) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ ui.text("Exposure Compensation");
+ var hExpComp = Id.handle();
+ hExpComp.nest(8006);
+ hExpComp.value = expComp;
+ expComp = ui.slider(hExpComp, formatValue(expComp, 1) + " EV", 0.0, 16.0, true, 100, true, Left, true);
+
+ if (hExpComp.changed) {
+ applyToRenderPipeline();
+ }
+
+ ui.separator();
+
+ var hAutoExp = Id.handle();
+ hAutoExp.nest(8009);
+ hAutoExp.selected = autoExposure;
+
+ ui.row([1/5, 4/5]);
+ autoExposure = ui.check(hAutoExp, "");
+ ui.text("Auto Exposure");
+
+ if (hAutoExp.changed) {
+ applyToRenderPipeline();
+ }
+ ui.separator();
+
+ var ev = calculateExposureValue();
+ ui.text("Exposure Value: EV " + formatValue(ev, 2));
+ ui.separator();
+ var hReset = Id.handle();
+ hReset.nest(8007);
+ if (ui.button("Reset to Defaults")) {
+ resetToDefaults();
+ }
+ ui.separator();
+ var hClose = Id.handle();
+ hClose.nest(8008);
+ if (ui.button("Close Panel")) {
+ controlsVisible = false;
+ Input.occupied = false;
+ }
+ }
+ }
+ function applyToRenderPipeline() {
+ if (camera == null) return;
+ Postprocess.camera_uniforms[0] = fStop;
+ Postprocess.camera_uniforms[1] = shutterSpeed;
+ Postprocess.camera_uniforms[2] = iso;
+ Postprocess.camera_uniforms[3] = expComp;
+ Postprocess.camera_uniforms[5] = autoFocus ? 1 : 0;
+ Postprocess.camera_uniforms[6] = focusDistance;
+ Postprocess.camera_uniforms[7] = fovToFocalLength(fov);
+ Postprocess.camera_uniforms[8] = fStop;
+ Postprocess.auto_exposure_uniforms[0] = autoExposure ? 1 : 0;
+ var focalLengthMM = fovToFocalLength(fov);
+ var apertureDiameter = focalLengthMM / fStop;
+ }
+
+ function applyFOVToCamera() {
+ if (camera == null) return;
+
+ var fovRadians = fov * (Math.PI / 180.0);
+ camera.data.raw.fov = fovRadians;
+ camera.buildProjection();
+
+ var focalLengthEquiv = fovToFocalLength(fov);
+ }
+
+
+ function calculateExposureValue(): Float {
+ // EV = log2(N^2 / t) where N = f-stop, t = shutter time
+ var shutterTime = 1.0 / shutterSpeed;
+ var ev = Math.log(fStop * fStop / shutterTime) / Math.log(2.0);
+ var isoAdjust = Math.log(iso / 100.0) / Math.log(2.0);
+ ev += isoAdjust;
+
+ return ev;
+ }
+
+ function fovToFocalLength(fovDegrees: Float): Float {
+ var fovRadians = fovDegrees * (Math.PI / 180.0);
+ var focalLength = sensorSize / (2.0 * Math.tan(fovRadians / 2.0));
+ return focalLength;
+ }
+
+ function resetToDefaults() {
+ fStop = defaultFStop;
+ shutterSpeed = defaultShutterSpeed;
+ iso = defaultISO;
+ fov = defaultFOV;
+ autoExposure = defaultAutoExposure;
+ autoFocus = defaultAutoFocus;
+ focusDistance = defaultFocusDistance;
+ expComp = exposureCompensation;
+ applyToRenderPipeline();
+ applyFOVToCamera();
+ }
+
+ function formatValue(value: Float, decimals: Int): String {
+ var multiplier = Math.pow(10, decimals);
+ var rounded = Math.round(value * multiplier) / multiplier;
+ return Std.string(rounded);
+ }
+}
diff --git a/leenkx/Sources/leenkx/trait/LightControl.hx b/leenkx/Sources/leenkx/trait/LightControl.hx
new file mode 100644
index 0000000..767fbad
--- /dev/null
+++ b/leenkx/Sources/leenkx/trait/LightControl.hx
@@ -0,0 +1,307 @@
+package leenkx.trait;
+
+import iron.Trait;
+import iron.Scene;
+import iron.object.LightObject;
+import iron.system.Input;
+import kha.graphics2.Graphics;
+import zui.Zui;
+import zui.Id;
+import iron.data.Data;
+import leenkx.ui.Canvas;
+
+/**
+ * Light Controls Trait for parameter controls via ZUI
+ */
+
+class LightControl extends Trait {
+
+ @prop var enableUI: Bool = true;
+ @prop var toggleKey: String = "l";
+
+ var ui: Zui;
+ var controlsVisible: Bool = false;
+ var lights: Array = [];
+ var worldStrength: Float = 1.0;
+ var worldProbe: iron.data.WorldData = null;
+ var scrollHandle = new zui.Zui.Handle();
+
+ public function new() {
+ super();
+ notifyOnInit(init);
+ notifyOnUpdate(update);
+ notifyOnRender2D(render);
+ }
+
+ function init() {
+ if (Scene.active.world != null) {
+ worldProbe = Scene.active.world;
+ if (worldProbe.raw.probe != null) {
+ worldStrength = worldProbe.raw.probe.strength;
+ }
+ }
+ }
+
+ function update() {
+ if (!enableUI) return;
+
+ if (Input.getKeyboard().started(toggleKey)) {
+ controlsVisible = !controlsVisible;
+
+ if (controlsVisible) {
+ gatherLights();
+ }
+ if (controlsVisible && ui == null) {
+ Data.getFont(Canvas.defaultFontName, function(font: kha.Font) {
+ ui = new Zui({font: font, scaleFactor: 1.0});
+ });
+ }
+ }
+ Input.occupied = controlsVisible;
+ }
+
+ function render(g: Graphics) {
+ if (!controlsVisible || ui == null) return;
+ g.end();
+ ui.begin(g);
+ drawControlPanel();
+ ui.end();
+ g.begin(false);
+ }
+
+ function gatherLights() {
+ lights = [];
+ for (light in Scene.active.lights) {
+ if (light != null && light.visible) {
+ lights.push(light);
+ }
+ }
+ }
+
+ function drawControlPanel() {
+ var h = Id.handle();
+ h.nest(9000);
+ h.redraws = 1;
+
+ var panelWidth = 450;
+ var panelHeight = Std.int(iron.App.h() * 0.9);
+ var panelX = Std.int(iron.App.w() - panelWidth - 20);
+ var panelY = Std.int((iron.App.h() - panelHeight) / 2);
+
+ if (ui.window(h, panelX, panelY, panelWidth, panelHeight, true)) {
+ ui.text("LIGHT & SHADOW CONTROLS");
+ ui.separator();
+
+ var hRefresh = Id.handle();
+ hRefresh.nest(9001);
+ if (ui.button("Refresh Lights")) {
+ gatherLights();
+ }
+
+ ui.separator();
+
+ drawWorldControls();
+
+ ui.separator();
+ ui.separator();
+
+ ui.text("Lights in Scene: " + lights.length);
+ ui.separator();
+
+ var hScroll = Id.handle();
+ hScroll.nest(9002);
+
+ for (i in 0...lights.length) {
+ drawLightControls(lights[i], i);
+ if (i < lights.length - 1) {
+ ui.separator();
+ ui.separator();
+ }
+ }
+
+ ui.separator();
+ ui.separator();
+
+ var hClose = Id.handle();
+ hClose.nest(9999);
+ if (ui.button("Close Panel")) {
+ controlsVisible = false;
+ Input.occupied = false;
+ }
+ }
+ }
+
+ function drawWorldControls() {
+ ui.text("-- WORLD ENVIRONMENT --", Align.Center);
+ ui.separator();
+
+ if (worldProbe == null || worldProbe.raw.probe == null) {
+ ui.text("No world probe found");
+ return;
+ }
+
+ var hWorldStr = Id.handle();
+ hWorldStr.nest(9100);
+ hWorldStr.value = worldStrength;
+ worldStrength = ui.slider(hWorldStr, "Strength: " + formatValue(worldStrength, 2), 0.0, 10.0, true, 100, true, Left, true);
+
+ if (hWorldStr.changed) {
+ worldProbe.raw.probe.strength = worldStrength;
+ }
+ }
+
+ function drawLightControls(light: LightObject, index: Int) {
+ var lightNameHash = 0;
+ for (i in 0...light.name.length) {
+ lightNameHash = ((lightNameHash << 5) - lightNameHash) + light.name.charCodeAt(i);
+ lightNameHash = lightNameHash & lightNameHash;
+ }
+ var baseId: Int = Std.int(Math.abs(lightNameHash)) + (index * 100);
+
+ ui.text(light.name + " (" + light.data.raw.type.toUpperCase() + ")", Align.Left);
+ ui.separator();
+
+ var hStrength = Id.handle().nest(baseId + 0, {value: light.data.raw.strength});
+ var newStrength = ui.slider(hStrength, "Strength: " + formatValue(hStrength.value, 2),
+ 0.0, 100.0, true, 100, true, Left, true);
+
+ if (hStrength.changed) {
+ light.data.raw.strength = newStrength;
+ }
+
+ ui.text("Color:");
+
+ var hColorR = Id.handle().nest(baseId + 1, {value: light.data.raw.color[0]});
+ var newColorR = ui.slider(hColorR, "R: " + formatValue(hColorR.value, 2),
+ 0.0, 1.0, true, 100, true, Left, true);
+ if (hColorR.changed) light.data.raw.color[0] = newColorR;
+
+ var hColorG = Id.handle().nest(baseId + 2, {value: light.data.raw.color[1]});
+ var newColorG = ui.slider(hColorG, "G: " + formatValue(hColorG.value, 2),
+ 0.0, 1.0, true, 100, true, Left, true);
+ if (hColorG.changed) light.data.raw.color[1] = newColorG;
+
+ var hColorB = Id.handle().nest(baseId + 3, {value: light.data.raw.color[2]});
+ var newColorB = ui.slider(hColorB, "B: " + formatValue(hColorB.value, 2),
+ 0.0, 1.0, true, 100, true, Left, true);
+ if (hColorB.changed) light.data.raw.color[2] = newColorB;
+
+ if (light.data.raw.cast_shadow != null) {
+ var hCastShadow = Id.handle().nest(baseId + 4);
+ hCastShadow.selected = light.data.raw.cast_shadow;
+
+ ui.row([1/4, 3/4]);
+ light.data.raw.cast_shadow = ui.check(hCastShadow, "");
+ ui.text("Cast Shadow");
+ }
+
+ if (light.data.raw.cast_shadow == true) {
+ ui.separator();
+ ui.text("— Shadow Settings —", Align.Center);
+
+ if (light.data.raw.shadows_bias != null) {
+ var hBias = Id.handle().nest(baseId + 5, {value: light.data.raw.shadows_bias * 10000.0});
+ var biasDisplay = ui.slider(hBias, "Bias: " + formatValue(hBias.value, 1),
+ 0.0, 100.0, true, 100, true, Left, true);
+
+ if (hBias.changed) {
+ light.data.raw.shadows_bias = biasDisplay / 10000.0; // Convert back
+ trace("[LightControl] " + light.name + " Bias: " + light.data.raw.shadows_bias);
+ }
+ }
+
+ if (light.data.raw.near_plane != null) {
+ var hNear = Id.handle().nest(baseId + 6, {value: light.data.raw.near_plane});
+ var newNear = ui.slider(hNear, "Near: " + formatValue(hNear.value, 3),
+ 0.001, 10.0, true, 1000, true, Left, true);
+
+ if (hNear.changed) {
+ light.data.raw.near_plane = newNear;
+ trace("[LightControl] " + light.name + " Near Plane: " + light.data.raw.near_plane);
+ light.buildMatrix(Scene.active.camera);
+ }
+ }
+
+ if (light.data.raw.far_plane != null) {
+ var hFar = Id.handle().nest(baseId + 7, {value: light.data.raw.far_plane});
+ var newFar = ui.slider(hFar, "Far: " + formatValue(hFar.value, 1),
+ 1.0, 1000.0, true, 100, true, Left, true);
+
+ if (hFar.changed) {
+ light.data.raw.far_plane = newFar;
+ trace("[LightControl] " + light.name + " Far Plane: " + light.data.raw.far_plane);
+ light.buildMatrix(Scene.active.camera);
+ }
+ }
+
+ if (light.data.raw.shadowmap_size != null) {
+ ui.text("ShadowMap: " + light.data.raw.shadowmap_size + "px");
+ }
+ }
+
+ if (light.data.raw.type == "spot") {
+ ui.separator();
+ ui.text("— Spot Settings —", Align.Center);
+
+ if (light.data.raw.spot_size != null) {
+ // spot_size is stored as cos(angle), convert to degrees for UI
+ var angleRad = Math.acos(light.data.raw.spot_size);
+ var angleDeg = angleRad * (180.0 / Math.PI) * 2.0; // Full cone angle
+ var hSpotSize = Id.handle().nest(baseId + 8, {value: angleDeg});
+
+ angleDeg = ui.slider(hSpotSize, "Cone: " + formatValue(hSpotSize.value, 1) + "°",
+ 1.0, 179.0, true, 100, true, Left, true);
+
+ if (hSpotSize.changed) {
+ var halfAngleRad = (angleDeg / 2.0) * (Math.PI / 180.0);
+ light.data.raw.spot_size = Math.cos(halfAngleRad);
+ trace("[LightControl] " + light.name + " Spot Cone: " + angleDeg + "°");
+ }
+ }
+
+ if (light.data.raw.spot_blend != null) {
+ var hSpotBlend = Id.handle().nest(baseId + 9, {value: light.data.raw.spot_blend * 10.0});
+ var blendDisplay = ui.slider(hSpotBlend, "Blend: " + formatValue(hSpotBlend.value, 2),
+ 0.0, 1.0, true, 100, true, Left, true);
+ if (hSpotBlend.changed) {
+ light.data.raw.spot_blend = blendDisplay / 10.0;
+ }
+ }
+
+ if (light.data.raw.fov != null) {
+ var fovDeg = light.data.raw.fov * (180.0 / Math.PI);
+ var hFOV = Id.handle().nest(baseId + 10, {value: fovDeg});
+ fovDeg = ui.slider(hFOV, "FOV: " + formatValue(hFOV.value, 1) + "°",
+ 10.0, 170.0, true, 100, true, Left, true);
+
+ if (hFOV.changed) {
+ light.data.raw.fov = fovDeg * (Math.PI / 180.0);
+ trace("[LightControl] " + light.name + " FOV: " + fovDeg + "°");
+ light.buildMatrix(Scene.active.camera);
+ }
+ }
+ }
+
+ if (light.data.raw.type == "point") {
+ ui.separator();
+ ui.text("— Point Light Settings —", Align.Center);
+
+ if (light.data.raw.fov != null) {
+ ui.text("FOV: " + formatValue(light.data.raw.fov * (180.0 / Math.PI), 1) + "° (90° for cubemap)");
+ }
+ }
+
+ if (light.data.raw.type == "sun") {
+ ui.separator();
+ ui.text("— Sun Settings —", Align.Center);
+ ui.text("Directional light source");
+ ui.text("Shadow cascades: " + LightObject.cascadeCount);
+ }
+ }
+
+ function formatValue(value: Float, decimals: Int): String {
+ var multiplier = Math.pow(10, decimals);
+ var rounded = Math.round(value * multiplier) / multiplier;
+ return Std.string(rounded);
+ }
+}