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); + } +}