From 0adcafd697087477e18998e53c8e4ac73855cf22 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Sat, 21 Feb 2026 22:17:44 -0800 Subject: [PATCH] Patch_1 --- leenkx/Sources/iron/App.hx | 34 ++ leenkx/Sources/iron/RenderPath.hx | 374 ++++++++++++++++-- leenkx/Sources/iron/Scene.hx | 7 +- leenkx/Sources/iron/data/Armature.hx | 4 +- leenkx/Sources/iron/object/Animation.hx | 8 +- leenkx/Sources/iron/object/BoneAnimation.hx | 36 +- leenkx/Sources/iron/object/CameraObject.hx | 14 +- leenkx/Sources/iron/object/LightObject.hx | 50 ++- leenkx/Sources/iron/object/MorphTarget.hx | 104 ++++- leenkx/Sources/iron/object/Object.hx | 8 +- leenkx/Sources/iron/object/ObjectAnimation.hx | 4 +- leenkx/Sources/iron/object/Uniforms.hx | 233 ++++++++++- leenkx/Sources/iron/system/Time.hx | 26 ++ leenkx/Sources/iron/system/VRController.hx | 138 +++++++ 14 files changed, 981 insertions(+), 59 deletions(-) create mode 100644 leenkx/Sources/iron/system/VRController.hx diff --git a/leenkx/Sources/iron/App.hx b/leenkx/Sources/iron/App.hx index 3d6bd78..46319ce 100644 --- a/leenkx/Sources/iron/App.hx +++ b/leenkx/Sources/iron/App.hx @@ -53,6 +53,21 @@ class App { static function update() { if (Scene.active == null || !Scene.active.ready) return; + // VR is handling it so we prevent double updates + // TODO: avoid js.Syntax + #if (kha_webgl && lnx_vr) + var vrActive = false; + js.Syntax.code(" + if (typeof kha !== 'undefined' && kha.vr && kha.vr.VrInterface) { + const vr = kha.vr.VrInterface.instance; + if (vr && vr.IsPresenting && vr.IsPresenting()) { + {0} = true; + } + } + ", vrActive); + if (vrActive) return; + #end + iron.system.Time.update(); if (lastw == -1) { @@ -138,6 +153,21 @@ class App { traitInits.splice(0, traitInits.length); } + // skip for XR callback to handle rendering + // TODO: avoid js Syntax + #if (kha_webgl && lnx_vr) + var vrActive = false; + js.Syntax.code(" + if (typeof kha !== 'undefined' && kha.vr && kha.vr.VrInterface) { + const vr = kha.vr.VrInterface.instance; + if (vr && vr.IsPresenting && vr.IsPresenting()) { + {0} = true; + } + } + ", vrActive); + + if (!vrActive) { + #end Scene.active.renderFrame(frame.g4); for (f in traitRenders) { @@ -146,6 +176,10 @@ class App { render2D(frame); + #if (kha_webgl && lnx_vr) + } + #end + #if lnx_debug renderPathTime = kha.Scheduler.realTime() - startTime; #end diff --git a/leenkx/Sources/iron/RenderPath.hx b/leenkx/Sources/iron/RenderPath.hx index 6cc7e2a..f3b006a 100644 --- a/leenkx/Sources/iron/RenderPath.hx +++ b/leenkx/Sources/iron/RenderPath.hx @@ -18,10 +18,44 @@ import iron.object.LightObject; import iron.object.MeshObject; import iron.object.Uniforms; import iron.object.Clipmap; +#if lnx_vr +import iron.math.Vec4; +import iron.math.Mat4; +import iron.math.Quat; +#end class RenderPath { public static var active: RenderPath; + #if lnx_vr + static var vrSimulateMode: Bool = false; + static var vrCameraOffsetSet:Bool = false; + static var vrCameraOffset:Vec4 = new Vec4(); + static var wasVRPresenting:Bool = false; + public static var vrCalibrationPosition:Vec4 = null; + public static var vrCalibrationRotation:iron.math.Quat = null; + public static var vrCalibrationSaved:Bool = false; + + public static var vrCenterCameraWorld:Mat4 = null; + + static var vrOriginalSuperSample:Float = -1.0; + public static inline function isVRPresenting(): Bool { + #if (kha_webgl && lnx_vr) + return kha.vr.VrInterface.instance != null && kha.vr.VrInterface.instance.IsPresenting(); + #else + return false; + #end + } + + public static inline function isVRSimulateMode(): Bool { + return vrSimulateMode; + } + + // TODO: done remove safely + public static inline function debugLog(msg: String, once: Bool = true): Void { + return; + } + #end public var frameScissor = false; public var frameScissorX = 0; @@ -43,9 +77,15 @@ class RenderPath { public var isProbe = false; public var currentG: Graphics = null; public var frameG: Graphics; + #if lnx_vr + var beginCalled = false; + var scissorSet = false; + var viewportScaled = false; + var renderToXRFramebuffer = false; + #end public var drawOrder = DrawOrder.Distance; public var paused = false; - public var ready(get, null): Bool; + public var ready(get, never): Bool; function get_ready(): Bool { return loading == 0; } public var commands: Void->Void = null; public var setupDepthTexture: Void->Void = null; @@ -123,9 +163,93 @@ class RenderPath { public function renderFrame(g: Graphics) { if (!ready || paused || iron.App.w() == 0 || iron.App.h() == 0) return; - if (lastW > 0 && (lastW != iron.App.w() || lastH != iron.App.h())) resize(); - lastW = iron.App.w(); - lastH = iron.App.h(); + var appW = iron.App.w(); + var appH = iron.App.h(); + + // use native XR framebuffer dimensions + #if (kha_webgl && lnx_vr) + if (kha.vr.VrInterface.instance != null) { + var vr = kha.vr.VrInterface.instance; + var isPresenting = vr != null && vr.IsPresenting(); + + // save/restore camera position between modes + if (!wasVRPresenting && isPresenting) { + if (Scene.active != null && Scene.active.camera != null) { + if (vrCalibrationPosition == null) vrCalibrationPosition = new Vec4(); + if (vrCalibrationRotation == null) vrCalibrationRotation = new Quat(); + + vrCalibrationPosition.setFrom(Scene.active.camera.transform.loc); + vrCalibrationRotation.setFrom(Scene.active.camera.transform.rot); + vrCalibrationSaved = true; + } + + // save original super sampling for later + vrOriginalSuperSample = leenkx.renderpath.Inc.superSample; + + // compositeToXR function handles blitting to VR framebuffer + + var xrVr: kha.js.vr.VrInterface = cast vr; + if (xrVr.xrGLLayer != null) { + var vrWidth = untyped xrVr.xrGLLayer.framebufferWidth; + var vrHeight = untyped xrVr.xrGLLayer.framebufferHeight; + } + } + else if (wasVRPresenting && !isPresenting) { + // reset VR frame time before anything else + #if (kha_webgl && lnx_vr) + iron.system.Time.vrFrameTime = -1.0; + #end + + if (vrCalibrationSaved && Scene.active != null && Scene.active.camera != null) { + Scene.active.camera.transform.loc.setFrom(vrCalibrationPosition); + Scene.active.camera.transform.rot.setFrom(vrCalibrationRotation); + Scene.active.camera.buildMatrix(); + Scene.active.camera.buildProjection(); + } + + // restore original super sampling from simulate mode + if (vrOriginalSuperSample >= 0.0) { + leenkx.renderpath.Inc.superSample = vrOriginalSuperSample; + + for (rt in renderTargets) { + if (rt.raw.width == 0 && rt.raw.scale != null) { + rt.raw.scale = vrOriginalSuperSample; + } + } + resize(); + vrOriginalSuperSample = -1.0; + } + + // reset offset for next session + vrCameraOffsetSet = false; + vrCameraOffset = null; + } + wasVRPresenting = isPresenting; + + if (isPresenting) { + // TODO: re-investigate using super sampling to avoid pixelation in simulate mode while giving max quality in headset + if (vrOriginalSuperSample >= 0.0 && leenkx.renderpath.Inc.superSample != 4.0) { + leenkx.renderpath.Inc.superSample = 4.0; + for (rt in renderTargets) { + if (rt.raw.width == 0 && rt.raw.scale != null) { + rt.raw.scale = 4.0; + } + } + resize(); + } + + var xrVr: kha.js.vr.VrInterface = cast vr; + if (xrVr.xrGLLayer != null) { + appW = xrVr.xrGLLayer.framebufferWidth; + appH = xrVr.xrGLLayer.framebufferHeight; + } + } + } + #end + + if (lastW > 0 && (lastW != appW || lastH != appH)) resize(); + lastW = appW; + lastH = appH; frameTime = Time.time() - lastFrameTime; lastFrameTime = Time.time(); @@ -191,7 +315,9 @@ class RenderPath { } light = Scene.active.lights[0]; - commands(); + if (commands != null) { + commands(); + } if (!isProbe) frame++; } @@ -207,13 +333,13 @@ class RenderPath { begin(frameG, Scene.active.camera.currentFace); } else { // Screen, planar probe - currentW = iron.App.w(); - currentH = iron.App.h(); + currentW = kha.System.windowWidth(); + currentH = kha.System.windowHeight(); if (frameScissor) setFrameScissor(); begin(frameG); if (!isProbe) { - setCurrentViewport(iron.App.w(), iron.App.h()); - setCurrentScissor(iron.App.w(), iron.App.h()); + setCurrentViewport(kha.System.windowWidth(), kha.System.windowHeight()); + setCurrentScissor(kha.System.windowWidth(), kha.System.windowHeight()); } } } @@ -258,16 +384,42 @@ class RenderPath { if (currentG != null) end(); currentG = g; additionalTargets = additionalRenderTargets; - face >= 0 ? g.beginFace(face) : g.begin(additionalRenderTargets); + + // we still bind but skip begin() when explicitly rendering to XR framebuffer (renderToXRFramebuffer flag) + #if lnx_vr + if (!renderToXRFramebuffer) { + face >= 0 ? g.beginFace(face) : g.begin(additionalRenderTargets); + beginCalled = true; + } else { + // XR framebuffer is already bound by VrInterface so we dont rebind + beginCalled = false; + } + #else + face >= 0 ? g.beginFace(face) : g.begin(additionalRenderTargets); + #end } inline function end() { + if (currentG == null) return; if (scissorSet) { currentG.disableScissor(); scissorSet = false; } + + #if lnx_vr + if (beginCalled) { + currentG.end(); + beginCalled = false; + } + // persist for rendering both eyes + if (!isVRPresenting()) { + currentG = null; + additionalTargets = null; + } + #else currentG.end(); currentG = null; + #end bindParams = null; } @@ -341,8 +493,8 @@ class RenderPath { if (a.data.sortingIndex != b.data.sortingIndex) { return a.data.sortingIndex > b.data.sortingIndex ? 1 : -1; } - - return a.data.name >= b.data.name ? 1 : -1; }); + return a.data.name >= b.data.name ? 1 : -1; + }); } public function drawMeshes(context: String) { @@ -521,44 +673,208 @@ class RenderPath { return Reflect.field(kha.Shaders, handle + "_comp"); } + #if lnx_vr + // blits to each eyes viewport in the XR framebuffer. + public function compositeToXR(sourceTarget: String) { + #if (kha_webgl && lnx_vr) + + var vr: kha.js.vr.VrInterface = cast kha.vr.VrInterface.instance; + if (vr == null || vr._glContext == null || vr.xrGLLayer == null) { + return; + } + + var gl: js.html.webgl.WebGL2RenderingContext = cast vr._glContext; + var source = renderTargets.get(sourceTarget); + if (source == null) { + return; + } + var sourceFB: js.html.webgl.Framebuffer = untyped source.image.g4.renderTargetFrameBuffer; + if (sourceFB == null) { + return; + } + // trace('Framebuffer OK'); + + renderToXRFramebuffer = true; + gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.DRAW_FRAMEBUFFER, vr.xrGLLayer.framebuffer); + gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.READ_FRAMEBUFFER, sourceFB); + + var readStatus = gl.checkFramebufferStatus(js.html.webgl.WebGL2RenderingContext.READ_FRAMEBUFFER); + var drawStatus = gl.checkFramebufferStatus(js.html.webgl.WebGL2RenderingContext.DRAW_FRAMEBUFFER); + if (readStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE || + drawStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE) { + return; + } + + var halfWidth = Std.int(source.image.width / 2); + var fullHeight = source.image.height; + + if (vr._leftViewport != null) { + var vp = vr._leftViewport; + gl.blitFramebuffer( + 0, 0, halfWidth, fullHeight, + vp.x, vp.y, vp.x + vp.width, vp.y + vp.height, + js.html.webgl.WebGL2RenderingContext.COLOR_BUFFER_BIT, + js.html.webgl.WebGL2RenderingContext.LINEAR + ); + } + if (vr._rightViewport != null) { + var vp = vr._rightViewport; + gl.blitFramebuffer( + halfWidth, 0, source.image.width, fullHeight, + vp.x, vp.y, vp.x + vp.width, vp.y + vp.height, + js.html.webgl.WebGL2RenderingContext.COLOR_BUFFER_BIT, + js.html.webgl.WebGL2RenderingContext.LINEAR + ); + } + + gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER, null); + renderToXRFramebuffer = false; + #end + } + #end #if lnx_vr public function drawStereo(drawMeshes: Void->Void) { - var vr = kha.vr.VrInterface.instance; + vrSimulateMode = false; + + if (currentG == null && frameG != null) { + currentG = frameG; + } + var appw = iron.App.w(); var apph = iron.App.h(); - var halfw = Std.int(appw / 2); var g = currentG; + + // get render target dimensions not App.w/h gbuffer is scaled in simulate mode with supersampling - if (vr != null && vr.IsPresenting()) { - // Left eye - Scene.active.camera.V.setFrom(Scene.active.camera.leftV); + var gbuffer0 = renderTargets.get("gbuffer0"); + var actualWidth = (gbuffer0 != null && gbuffer0.image != null) ? gbuffer0.image.width : appw; + var actualHeight = (gbuffer0 != null && gbuffer0.image != null) ? gbuffer0.image.height : apph; + var actualHalfWidth = Std.int(actualWidth / 2); + + var vrFBWidth = actualWidth; + var vrFBHeight = actualHeight; + var vrHalfWidth = actualHalfWidth; + var isVRPresenting = false; + vrSimulateMode = false; + + var vr:Dynamic = null; + var vrExists = false; + #if (kha_webgl && lnx_vr) + if (kha.vr.VrInterface.instance != null) { + vr = kha.vr.VrInterface.instance; + vrExists = true; + } + #end + + if (vrExists && vr != null && vr.IsPresenting()) { + vrSimulateMode = false; + isVRPresenting = true; + + // get framebuffer dimensions from XR layer + #if (kha_webgl && lnx_vr) + var xrVr: kha.js.vr.VrInterface = cast vr; + if (xrVr.xrGLLayer != null) { + vrFBWidth = untyped xrVr.xrGLLayer.framebufferWidth; + vrFBHeight = untyped xrVr.xrGLLayer.framebufferHeight; + vrHalfWidth = Std.int(vrFBWidth / 2); + } + #end + + if (Scene.active == null || Scene.active.camera == null) { + return; + } + + #if (kha_webgl && lnx_vr) + if (vrCenterCameraWorld == null) vrCenterCameraWorld = Mat4.identity(); + vrCenterCameraWorld.setFrom(Scene.active.camera.transform.world); + #end + + // LEFT EYE + // HMD center for room scale position tracking + #if (kha_webgl && lnx_vr) + var xrVr: kha.js.vr.VrInterface = cast vr; + if (xrVr.currentViewerPose != null) { + var viewerTransform = untyped xrVr.currentViewerPose.transform; + if (viewerTransform != null && viewerTransform.position != null) { + // VR present calibration is used to position objects in world space not the camera + var pos = viewerTransform.position; + + // camera follows headset directly in local floor space + Scene.active.camera.transform.loc.set(pos.x, pos.y, pos.z); + + if (viewerTransform.orientation != null) { + Scene.active.camera.transform.rot.set( + viewerTransform.orientation.x, + viewerTransform.orientation.y, + viewerTransform.orientation.z, + viewerTransform.orientation.w + ); + } + Scene.active.camera.transform.buildMatrix(); + } + } + iron.system.VRController.updatePoses(); + #end + + Scene.active.camera.V.self = vr.GetViewMatrix(0); Scene.active.camera.P.self = vr.GetProjectionMatrix(0); - g.viewport(0, 0, halfw, apph); + Scene.active.camera.VP.setFrom(Scene.active.camera.P); + Scene.active.camera.VP.multmat(Scene.active.camera.V); + Scene.active.camera.buildMatrix(); // update frustum for culling + + var renderWidth = actualWidth; + var renderHeight = actualHeight; + var renderHalfWidth = actualHalfWidth; + + // left half of render target + g.viewport(0, 0, renderHalfWidth, renderHeight); + g.scissor(0, 0, renderHalfWidth, renderHeight); drawMeshes(); - // Right eye - begin(g, additionalTargets); - Scene.active.camera.V.setFrom(Scene.active.camera.rightV); + // RIGHT EYE + Scene.active.camera.V.self = vr.GetViewMatrix(1); Scene.active.camera.P.self = vr.GetProjectionMatrix(1); - g.viewport(halfw, 0, halfw, apph); + Scene.active.camera.VP.setFrom(Scene.active.camera.P); + Scene.active.camera.VP.multmat(Scene.active.camera.V); + Scene.active.camera.buildMatrix(); + + // right half of render target + g.viewport(renderHalfWidth, 0, renderHalfWidth, renderHeight); + g.scissor(renderHalfWidth, 0, renderHalfWidth, renderHeight); drawMeshes(); + + // restore for post-processing + g.disableScissor(); + g.viewport(0, 0, renderWidth, renderHeight); } else { // Simulate - Scene.active.camera.buildProjection(halfw / apph); + vrSimulateMode = true; + var ipd_offset = 0.032 * 35.0; - // Left eye - g.viewport(0, 0, halfw, apph); + #if (kha_webgl && lnx_vr) + if (vrCenterCameraWorld == null) vrCenterCameraWorld = Mat4.identity(); + vrCenterCameraWorld.setFrom(Scene.active.camera.transform.world); + #end + + Scene.active.camera.buildProjection(actualHalfWidth / actualHeight); + + Scene.active.camera.transform.move(Scene.active.camera.right(), -ipd_offset); + Scene.active.camera.buildMatrix(); + g.viewport(0, 0, actualHalfWidth, actualHeight); + g.scissor(0, 0, actualHalfWidth, actualHeight); drawMeshes(); - // Right eye begin(g, additionalTargets); - Scene.active.camera.transform.move(Scene.active.camera.right(), 0.032); + Scene.active.camera.transform.move(Scene.active.camera.right(), ipd_offset * 2.0); Scene.active.camera.buildMatrix(); - g.viewport(halfw, 0, halfw, apph); + g.viewport(actualHalfWidth, 0, actualHalfWidth, actualHeight); + g.scissor(actualHalfWidth, 0, actualHalfWidth, actualHeight); drawMeshes(); - Scene.active.camera.transform.move(Scene.active.camera.right(), -0.032); + Scene.active.camera.transform.move(Scene.active.camera.right(), -ipd_offset); Scene.active.camera.buildMatrix(); + g.disableScissor(); + g.viewport(0, 0, actualWidth, actualHeight); } } #end diff --git a/leenkx/Sources/iron/Scene.hx b/leenkx/Sources/iron/Scene.hx index c0f1869..88e1ebf 100644 --- a/leenkx/Sources/iron/Scene.hx +++ b/leenkx/Sources/iron/Scene.hx @@ -954,7 +954,12 @@ class Scene { static function createTraitClassInstance(traitName: String, args: Array): Dynamic { var cname = Type.resolveClass(traitName); if (cname == null) return null; - return Type.createInstance(cname, args); + try { + return Type.createInstance(cname, args); + } catch() { + trace("Error creating trait: " + traitName); + return null; + } } function loadEmbeddedData(datas: Array, done: Void->Void) { diff --git a/leenkx/Sources/iron/data/Armature.hx b/leenkx/Sources/iron/data/Armature.hx index 21f204a..1a6f229 100644 --- a/leenkx/Sources/iron/data/Armature.hx +++ b/leenkx/Sources/iron/data/Armature.hx @@ -37,7 +37,9 @@ class Armature { } public function getAction(name: String): TAction { - for (a in actions) if (a.name == name) return a; + for (a in actions) { + if (a.name == name) return a; + } return null; } diff --git a/leenkx/Sources/iron/object/Animation.hx b/leenkx/Sources/iron/object/Animation.hx index 1a3ec9b..55bed4e 100644 --- a/leenkx/Sources/iron/object/Animation.hx +++ b/leenkx/Sources/iron/object/Animation.hx @@ -141,6 +141,7 @@ class Animation { sampler.cacheSet = false; sampler.trackEnd = false; + if (anim == null || anim.tracks == null || anim.tracks.length == 0) return; var track = anim.tracks[0]; if (frameIndex == -1) { @@ -442,7 +443,12 @@ class ActionSampler { */ public inline function setObjectAction(actionData: TObj) { this.actionData = [actionData]; - this.totalFrames = actionData.anim.tracks[0].frames.length; + if (actionData != null && actionData.anim != null && actionData.anim.tracks != null && actionData.anim.tracks.length > 0) { + this.totalFrames = actionData.anim.tracks[0].frames.length; + } + else { + this.totalFrames = 0; + } actionDataInit = true; } diff --git a/leenkx/Sources/iron/object/BoneAnimation.hx b/leenkx/Sources/iron/object/BoneAnimation.hx index 46e9696..21c866f 100644 --- a/leenkx/Sources/iron/object/BoneAnimation.hx +++ b/leenkx/Sources/iron/object/BoneAnimation.hx @@ -108,9 +108,11 @@ class BoneAnimation extends Animation { object.transform.rot.set(0, 0, 0, 1); object.transform.buildMatrix(); - var refs = mo.parent.raw.bone_actions; - if (refs != null && refs.length > 0) { - Data.getSceneRaw(refs[0], function(action: TSceneFormat) { play(action.name); }); + if (mo.parent != null && mo.parent.raw != null && mo.parent.raw.bone_actions != null) { + var refs = mo.parent.raw.bone_actions; + if (refs.length > 0) { + Data.getSceneRaw(refs[0], function(action: TSceneFormat) { play(action.name); }); + } } } if (armatureObject.raw.relative_bone_constraints) relativeBoneConstraints = true; @@ -183,8 +185,10 @@ class BoneAnimation extends Animation { } function setAction(action: String) { + if (armature == null) return; armature.initMats(); var a = armature.getAction(action); + if (a == null) return; skeletonBones = a.bones; skeletonMats = a.mats; if(! rootMotionCacheInit) skeletonMats.push(Mat4.identity()); @@ -193,8 +197,11 @@ class BoneAnimation extends Animation { } function getAction(action: String): Array { + if (armature == null) return null; armature.initMats(); - return armature.getAction(action).bones; + var a = armature.getAction(action); + if (a == null) return null; + return a.bones; } function multParent(i: Int, fasts: Array, bones: Array, mats: Array) { @@ -225,9 +232,9 @@ class BoneAnimation extends Animation { } override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.2, speed = 1.0, loop = true) { - super.play(action, onComplete, blendTime, speed, loop); if (action != "") { setAction(action); + super.play(action, onComplete, blendTime, speed, loop); var tempAnimParam = new ActionSampler(action); registerAction("tempAction", tempAnimParam); updateAnimation = function(mats){ @@ -239,6 +246,10 @@ class BoneAnimation extends Animation { override public function update(delta: FastFloat) { this.delta = delta; if (!isSkinned && skeletonBones == null) setAction(armature.actions[0].name); + + // TODO: double check skip culling for skinned meshes if they need animation updates for bounds + // if (object != null && !object.visible) return; + if (object != null && (!object.visible || object.culled)) return; if (skeletonBones == null || skeletonBones.length == 0) return; @@ -248,7 +259,6 @@ class BoneAnimation extends Animation { super.update(delta); if(updateAnimation != null) { - updateAnimation(skeletonMats); } @@ -401,6 +411,7 @@ class BoneAnimation extends Animation { } var bones = sampler.getBoneAction(); + if (bones == null) return; for(b in bones){ if (b.anim != null) { updateTrack(b.anim, sampler); @@ -410,13 +421,14 @@ class BoneAnimation extends Animation { } public function sampleAction(sampler: ActionSampler, actionMats: Array) { - if(! sampler.actionDataInit) { var bones = getAction(sampler.action); sampler.setBoneAction(bones); } var bones = sampler.getBoneAction(); + if (bones == null) return; + actionMats[skeletonBones.length].setIdentity(); var rootMotionEnabled = sampler.rootMotionPos || sampler.rootMotionRot; for (i in 0...bones.length) { @@ -427,7 +439,6 @@ class BoneAnimation extends Animation { updateAnimSampled(bones[i].anim, actionMats[i], sampler); } } - } function updateAnimSampled(anim: TAnimation, mm: Mat4, sampler: ActionSampler) { @@ -588,6 +599,9 @@ class BoneAnimation extends Animation { public override function getTotalFrames(sampler: ActionSampler): Int { var bones = getAction(sampler.action); + if (bones == null){ + return 0; + } var track = bones[0].anim.tracks[0]; return Std.int(track.frames[track.frames.length - 1] - track.frames[0]); } @@ -1048,9 +1062,9 @@ class BoneAnimation extends Animation { var rootLen = root.bone_length * rootMat.getScale().x; // Get distance form root to goal - var goalLen = Math.abs(Vec4.distance(rootMat.getLoc(), goal)); + var goalLen: FastFloat = Math.abs(Vec4.distance(rootMat.getLoc(), goal)); - var totalLength = effectorLen + rootLen; + var totalLength: FastFloat = effectorLen + rootLen; // Get tip location of effector bone var effectorTipPos = new Vec4().setFrom(effectorMat.look()).normalize(); @@ -1070,7 +1084,7 @@ class BoneAnimation extends Animation { // Get unit vector of effector bone var vectorEffector = new Vec4().setFrom(effectorMat.look()).normalize(); - + // Get dot product of vectors var dot = new Vec4().setFrom(vectorRootEffector).dot(vectorRoot); // Calmp between -1 and 1 diff --git a/leenkx/Sources/iron/object/CameraObject.hx b/leenkx/Sources/iron/object/CameraObject.hx index fb52933..61c2ef4 100644 --- a/leenkx/Sources/iron/object/CameraObject.hx +++ b/leenkx/Sources/iron/object/CameraObject.hx @@ -42,9 +42,10 @@ class CameraObject extends Object { this.data = data; - #if lnx_vr - iron.system.VR.initButton(); - #end + // dont just auto initialize VR button - headset trait controls VR + // #if lnx_vr + // iron.system.VR.initButton(); + // #end buildProjection(); @@ -85,7 +86,14 @@ class CameraObject extends Object { projectionJitter(); #end + // matrices are set by VR system so avoid rebuilding transforms unless its in preview/not presenting + #if (kha_webgl && lnx_vr) + if (@:privateAccess !RenderPath.isVRPresenting()) { + buildMatrix(); + } + #else buildMatrix(); + #end RenderPath.active.renderFrame(g); diff --git a/leenkx/Sources/iron/object/LightObject.hx b/leenkx/Sources/iron/object/LightObject.hx index 1c1908f..cede901 100644 --- a/leenkx/Sources/iron/object/LightObject.hx +++ b/leenkx/Sources/iron/object/LightObject.hx @@ -59,6 +59,9 @@ class LightObject extends Object { public static var clustersData: kha.Image = null; static var lpos = new Vec4(); public static var LWVPMatrixArray: Float32Array = null; + #if lnx_vr + static var originalLightPositions: Float32Array = null; + #end #end // lnx_clusters public var V: Mat4 = Mat4.identity(); @@ -519,7 +522,7 @@ class LightObject extends Object { updateLightsArray(); // TODO: only update on light change } - static function updateLightsArray() { + public static function updateLightsArray() { if (lightsArray == null) { // vec4x3 - 1: pos, a, color, b, 2: dir, c lightsArray = new Float32Array(maxLights * 4 * 3); #if lnx_spot @@ -578,6 +581,49 @@ class LightObject extends Object { } } + // VR deferred stereo we save original light positions before adjusting for per-eye rendering + #if lnx_vr + public static function saveOriginalLightPositions() { + if (lightsArray == null) return; + + if (originalLightPositions == null) { + originalLightPositions = new Float32Array(lightsArray.length); + } + + for (i in 0...lightsArray.length) { + originalLightPositions[i] = lightsArray[i]; + } + } + + // negative for left eye, positive for right eye + public static function adjustLightPositionsForVREye(offsetX: Float, rightVec: Vec4) { + if (lightsArray == null) return; + + var lights = Scene.active.lights; + var n = lights.length > maxLights ? maxLights : lights.length; + var i = 0; + + for (l in lights) { + if (discardLightCulled(l)) continue; + if (i >= n) break; + + lightsArray[i * 12 ] = originalLightPositions[i * 12 ] + rightVec.x * offsetX; + lightsArray[i * 12 + 1] = originalLightPositions[i * 12 + 1] + rightVec.y * offsetX; + lightsArray[i * 12 + 2] = originalLightPositions[i * 12 + 2] + rightVec.z * offsetX; + + i++; + } + } + + public static function restoreOriginalLightPositions() { + if (lightsArray == null || originalLightPositions == null) return; + + for (i in 0...lightsArray.length) { + lightsArray[i] = originalLightPositions[i]; + } + } + #end + public static function updateLWVPMatrixArray(object: Object, type: String) { if (LWVPMatrixArray == null) { LWVPMatrixArray = new Float32Array(maxLightsCluster * 16); @@ -629,8 +675,8 @@ class LightObject extends Object { LWVPMatrixArray[i * 16 + 13] = m._31; LWVPMatrixArray[i * 16 + 14] = m._32; LWVPMatrixArray[i * 16 + 15] = m._33; + i++; // only increment in light type } - i++; } return LWVPMatrixArray; } diff --git a/leenkx/Sources/iron/object/MorphTarget.hx b/leenkx/Sources/iron/object/MorphTarget.hx index e259fa9..e4288e2 100644 --- a/leenkx/Sources/iron/object/MorphTarget.hx +++ b/leenkx/Sources/iron/object/MorphTarget.hx @@ -20,6 +20,13 @@ class MorphTarget { public var morphDataPos: Image; public var morphDataNor: Image; public var morphMap: Map = null; + + public var isDirty: Bool = true; + var previousWeights: Float32Array; + var changeThreshold: FastFloat = 0.001; // skip smaller + var pendingUpdates: Map = null; + var batchUpdateEnabled: Bool = true; + var lastFlushFrame: Int = 0; public function new(data: TMorphTarget) { initWeights(data.morph_target_defaults); @@ -42,6 +49,14 @@ class MorphTarget { morphMap.set(name, i); i++; } + + previousWeights = new Float32Array(morphWeights.length); + for (i in 0...morphWeights.length) { + previousWeights.set(i, morphWeights.get(i)); + } + + // batch system + pendingUpdates = new Map(); } inline function initWeights(defaults: Float32Array) { @@ -54,9 +69,96 @@ class MorphTarget { public function setMorphValue(name: String, value: Float) { var i = morphMap.get(name); if (i != null) { - morphWeights.set(i, value); + if (batchUpdateEnabled) { + pendingUpdates.set(i, value); + } else { + setMorphValueDirect(i, value); + } } } + + // faster indexed access + public inline function setMorphValueDirect(index: Int, value: Float) { + var current = morphWeights.get(index); + + // allow explicit zero values to reset + if (value == 0.0 && current != 0.0) { + morphWeights.set(index, value); + isDirty = true; + return; + } + + var delta = value - current; + + if (delta < -changeThreshold || delta > changeThreshold) { + morphWeights.set(index, value); + isDirty = true; + } + } + + // flush pending batch + public function flushBatchedUpdates() { + if (pendingUpdates.keys().hasNext()) { + var anyChanged = false; + var hasZeros = false; + + for (index in pendingUpdates.keys()) { + var value = pendingUpdates.get(index); + if (value == null) continue; + + if (value == 0.0) hasZeros = true; + + var current = morphWeights.get(index); + var delta = value - current; + + if (value == 0.0 && current != 0.0) { + try{ + morphWeights.set(index, cast value); + }catch(e){ + trace("ERROR: " + e); + } + anyChanged = true; + } + else if (delta < -changeThreshold || delta > changeThreshold) { + morphWeights.set(index, cast value); + anyChanged = true; + } + } + + pendingUpdates.clear(); + + if (anyChanged || hasZeros) { + isDirty = true; + } + } + } + + public inline function markClean() { + isDirty = false; + for (i in 0...morphWeights.length) { + previousWeights.set(i, morphWeights.get(i)); + } + } + + public inline function markDirty() { + isDirty = true; + } + + // toggle batch mode + public inline function setBatchMode(enabled: Bool) { + if (!enabled && batchUpdateEnabled) { + flushBatchedUpdates(); + } + batchUpdateEnabled = enabled; + } + + public function resetAllWeights() { + for (i in 0...morphWeights.length) { + morphWeights.set(i, 0.0); + } + pendingUpdates.clear(); + isDirty = true; + } } #end diff --git a/leenkx/Sources/iron/object/Object.hx b/leenkx/Sources/iron/object/Object.hx index 4c4987b..570eec5 100644 --- a/leenkx/Sources/iron/object/Object.hx +++ b/leenkx/Sources/iron/object/Object.hx @@ -210,8 +210,12 @@ class Object { } #if lnx_skin - public function getBoneAnimation(armatureUid): BoneAnimation { - for (a in Scene.active.animations) if (a.armature != null && a.armature.uid == armatureUid) return cast a; + public function getBoneAnimation(armatureUid: Int): BoneAnimation { + for (a in Scene.active.animations) { + if (a.armature != null && a.armature.uid == armatureUid) { + return cast a; + } + } return null; } #else diff --git a/leenkx/Sources/iron/object/ObjectAnimation.hx b/leenkx/Sources/iron/object/ObjectAnimation.hx index 3d9b49f..036282a 100644 --- a/leenkx/Sources/iron/object/ObjectAnimation.hx +++ b/leenkx/Sources/iron/object/ObjectAnimation.hx @@ -97,7 +97,9 @@ class ObjectAnimation extends Animation { } public override function getTotalFrames(sampler: ActionSampler): Int { - var track = getAction(sampler.action).anim.tracks[0]; + var action = getAction(sampler.action); + if (action == null || action.anim == null || action.anim.tracks == null || action.anim.tracks.length == 0) return 0; + var track = action.anim.tracks[0]; return Std.int(track.frames[track.frames.length - 1] - track.frames[0]); } diff --git a/leenkx/Sources/iron/object/Uniforms.hx b/leenkx/Sources/iron/object/Uniforms.hx index 473958d..a395914 100644 --- a/leenkx/Sources/iron/object/Uniforms.hx +++ b/leenkx/Sources/iron/object/Uniforms.hx @@ -7,16 +7,20 @@ import kha.graphics4.TextureFilter; import kha.graphics4.MipMapFilter; import kha.arrays.Float32Array; import iron.math.Vec4; +import iron.math.Mat4; import iron.math.Quat; import iron.math.Mat3; -import iron.math.Mat4; -import iron.data.WorldData; import iron.data.MaterialData; import iron.data.ShaderData; -import iron.data.SceneFormat; +import iron.data.WorldData; +import iron.data.SceneFormat.TShaderConstant; +import iron.data.SceneFormat.TBindConstant; +import iron.object.Transform; +import iron.object.LightObject; +import iron.Scene; +import iron.RenderPath; import iron.system.Input; import iron.system.Time; -import iron.RenderPath; using StringTools; // Structure for setting shader uniforms @@ -38,6 +42,7 @@ class Uniforms { public static var helpMat = Mat4.identity(); public static var helpMat2 = Mat4.identity(); public static var helpMat3 = Mat3.identity(); + public static var helpMat4 = Mat4.identity(); public static var helpVec = new Vec4(); public static var helpVec2 = new Vec4(); public static var helpQuat = new Quat(); // Keep at identity @@ -47,6 +52,10 @@ class Uniforms { public static var externalVec4Links: ArrayMaterialData->String->Vec4> = null; public static var externalVec3Links: ArrayMaterialData->String->Vec4> = null; public static var externalVec2Links: ArrayMaterialData->String->Vec4> = null; + + public static var eyeLeftCallCount = 0; + public static var lastFrameChecked = -1; + public static var externalFloatLinks: ArrayMaterialData->String->Null> = null; public static var externalFloatsLinks: ArrayMaterialData->String->Float32Array> = null; public static var externalIntLinks: ArrayMaterialData->String->Null> = null; @@ -59,6 +68,10 @@ class Uniforms { public static var defaultFilter = TextureFilter.LinearFilter; #end + #if lnx_morph_target + public static var forceUploadMorphWeights: Bool = false; + #end + public static function setContextConstants(g: Graphics, context: ShaderContext, bindParams: Array) { if (context.raw.constants != null) { for (i in 0...context.raw.constants.length) { @@ -181,11 +194,15 @@ class Uniforms { // Multiple voxel volumes, always set params g.setImageTexture(context.textureUnits[j], rt.image); // image2D/3D if (rt.raw.name.startsWith("voxels_")) { - g.setTextureParameters(context.textureUnits[j], TextureAddressing.Clamp, TextureAddressing.Clamp, TextureFilter.LinearFilter, TextureFilter.LinearFilter, MipMapFilter.NoMipFilter); + g.setTextureParameters(context.textureUnits[j], TextureAddressing.Clamp, TextureAddressing.Clamp, TextureFilter.LinearFilter, TextureFilter.LinearFilter, MipMapFilter.LinearMipFilter); + } + else if (rt.raw.name.startsWith("voxelsSDF")) + { + g.setTexture3DParameters(context.textureUnits[j], TextureAddressing.Clamp, TextureAddressing.Clamp, TextureAddressing.Clamp, TextureFilter.PointFilter, TextureFilter.PointFilter, MipMapFilter.NoMipFilter); } else if (rt.raw.name.startsWith("voxels")) { - g.setTexture3DParameters(context.textureUnits[j], TextureAddressing.Clamp, TextureAddressing.Clamp, TextureAddressing.Clamp, TextureFilter.LinearFilter, TextureFilter.LinearFilter, MipMapFilter.NoMipFilter); + g.setTexture3DParameters(context.textureUnits[j], TextureAddressing.Clamp, TextureAddressing.Clamp, TextureAddressing.Clamp, TextureFilter.LinearFilter, TextureFilter.LinearFilter, MipMapFilter.PointMipFilter); } else { @@ -286,6 +303,89 @@ class Uniforms { helpMat.getInverse(helpMat); m = helpMat; } + #if lnx_vr + case "_inverseViewProjectionMatrixLeft": { + var vr = kha.vr.VrInterface.instance; + if (vr != null && vr.IsPresenting()) { + var leftView = vr.GetViewMatrix(0); + var leftProj = vr.GetProjectionMatrix(0); + helpMat._00 = leftView._00; helpMat._01 = leftView._01; helpMat._02 = leftView._02; helpMat._03 = leftView._03; + helpMat._10 = leftView._10; helpMat._11 = leftView._11; helpMat._12 = leftView._12; helpMat._13 = leftView._13; + helpMat._20 = leftView._20; helpMat._21 = leftView._21; helpMat._22 = leftView._22; helpMat._23 = leftView._23; + helpMat._30 = leftView._30; helpMat._31 = leftView._31; helpMat._32 = leftView._32; helpMat._33 = leftView._33; + helpMat2._00 = leftProj._00; helpMat2._01 = leftProj._01; helpMat2._02 = leftProj._02; helpMat2._03 = leftProj._03; + helpMat2._10 = leftProj._10; helpMat2._11 = leftProj._11; helpMat2._12 = leftProj._12; helpMat2._13 = leftProj._13; + helpMat2._20 = leftProj._20; helpMat2._21 = leftProj._21; helpMat2._22 = leftProj._22; helpMat2._23 = leftProj._23; + helpMat2._30 = leftProj._30; helpMat2._31 = leftProj._31; helpMat2._32 = leftProj._32; helpMat2._33 = leftProj._33; + helpMat.multmat(helpMat2); + helpMat.getInverse(helpMat); + } else if (iron.RenderPath.isVRSimulateMode()) { + var ipd_offset = 0.032 * 35.0; // Match eye offset + var rightVec = camera.rightWorld(); + + var eyeLeftX = camera.transform.worldx() - rightVec.x * ipd_offset; + var eyeLeftY = camera.transform.worldy() - rightVec.y * ipd_offset; + var eyeLeftZ = camera.transform.worldz() - rightVec.z * ipd_offset; + + helpMat.setFrom(camera.transform.world); + helpMat._30 = eyeLeftX; + helpMat._31 = eyeLeftY; + helpMat._32 = eyeLeftZ; + helpMat.getInverse(helpMat); // Now it's a view matrix + + helpMat.multmat(camera.P); + helpMat.getInverse(helpMat); + } else { + helpMat.setFrom(camera.V); + helpMat.multmat(camera.P); + helpMat.getInverse(helpMat); + } + m = helpMat; + } + case "_inverseViewProjectionMatrixRight": { + var vr = kha.vr.VrInterface.instance; + if (vr != null && vr.IsPresenting()) { + var rightView = vr.GetViewMatrix(1); + var rightProj = vr.GetProjectionMatrix(1); + // kha.math.FastMatrix4 to iron.math.Mat4 + helpMat2._00 = rightView._00; helpMat2._01 = rightView._01; helpMat2._02 = rightView._02; helpMat2._03 = rightView._03; + helpMat2._10 = rightView._10; helpMat2._11 = rightView._11; helpMat2._12 = rightView._12; helpMat2._13 = rightView._13; + helpMat2._20 = rightView._20; helpMat2._21 = rightView._21; helpMat2._22 = rightView._22; helpMat2._23 = rightView._23; + helpMat2._30 = rightView._30; helpMat2._31 = rightView._31; helpMat2._32 = rightView._32; helpMat2._33 = rightView._33; + helpMat4._00 = rightProj._00; helpMat4._01 = rightProj._01; helpMat4._02 = rightProj._02; helpMat4._03 = rightProj._03; + helpMat4._10 = rightProj._10; helpMat4._11 = rightProj._11; helpMat4._12 = rightProj._12; helpMat4._13 = rightProj._13; + helpMat4._20 = rightProj._20; helpMat4._21 = rightProj._21; helpMat4._22 = rightProj._22; helpMat4._23 = rightProj._23; + helpMat4._30 = rightProj._30; helpMat4._31 = rightProj._31; helpMat4._32 = rightProj._32; helpMat4._33 = rightProj._33; + helpMat2.multmat(helpMat4); + helpMat2.getInverse(helpMat2); + m = helpMat2; + } else if (iron.RenderPath.isVRSimulateMode()) { + var ipd_offset = 0.032 * 35.0; + var rightVec = camera.rightWorld(); + + // calculate right eye position in world space + var eyeRightX = camera.transform.worldx() + rightVec.x * ipd_offset; + var eyeRightY = camera.transform.worldy() + rightVec.y * ipd_offset; + var eyeRightZ = camera.transform.worldz() + rightVec.z * ipd_offset; + + helpMat2.setFrom(camera.transform.world); + helpMat2._30 = eyeRightX; + helpMat2._31 = eyeRightY; + helpMat2._32 = eyeRightZ; + helpMat2.getInverse(helpMat2); + + helpMat2.multmat(camera.P); + helpMat2.getInverse(helpMat2); + m = helpMat2; + } else { + // fallback to center camera + helpMat2.setFrom(camera.V); + helpMat2.multmat(camera.P); + helpMat2.getInverse(helpMat2); + m = helpMat2; + } + } + #end case "_viewProjectionMatrix": { #if lnx_centerworld m = vmat(camera.V); @@ -398,6 +498,28 @@ class Uniforms { v = helpVec; } } + #if lnx_vr + case "_pointPositionLeft": { + var point = RenderPath.active.point; + if (point != null) { + var lightWorldX = point.transform.worldx(); + var lightWorldY = point.transform.worldy(); + var lightWorldZ = point.transform.worldz(); + helpVec.set(lightWorldX, lightWorldY, lightWorldZ); + v = helpVec; + } + } + case "_pointPositionRight": { + var point = RenderPath.active.point; + if (point != null) { + var lightWorldX = point.transform.worldx(); + var lightWorldY = point.transform.worldy(); + var lightWorldZ = point.transform.worldz(); + helpVec.set(lightWorldX, lightWorldY, lightWorldZ); + v = helpVec; + } + } + #end #if lnx_spot case "_spotDirection": { var point = RenderPath.active.point; @@ -484,6 +606,84 @@ class Uniforms { helpVec = camera.rightWorld().normalize(); v = helpVec; } + #if lnx_vr + case "_eyeLeft": { + var currentFrame = iron.RenderPath.active.frame; + if (currentFrame != lastFrameChecked) { + eyeLeftCallCount = 0; + lastFrameChecked = currentFrame; + } + eyeLeftCallCount++; + + var vr = kha.vr.VrInterface.instance; + if (vr != null && vr.IsPresenting()) { + var leftViewMatrix = vr.GetViewMatrix(0); + var invLeft = leftViewMatrix.inverse(); + helpVec.set(invLeft._30, invLeft._31, invLeft._32); + // trace("eyeLeft: " + helpVec.x + ", " + helpVec.y + ", " + helpVec.z); + } else if (iron.RenderPath.isVRSimulateMode()) { + var ipd_offset = 0.032 * 35.0; + var rightVec = camera.rightWorld(); + var centerX = camera.transform.worldx(); + var centerY = camera.transform.worldy(); + var centerZ = camera.transform.worldz(); + helpVec.set( + centerX - rightVec.x * ipd_offset, + centerY - rightVec.y * ipd_offset, + centerZ - rightVec.z * ipd_offset + ); + } else { + helpVec.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz()); + } + v = helpVec; + } + case "_eyeRight": { + var vr = kha.vr.VrInterface.instance; + if (vr != null && vr.IsPresenting()) { + var rightViewMatrix = vr.GetViewMatrix(1); + var invRight = rightViewMatrix.inverse(); + helpVec.set(invRight._30, invRight._31, invRight._32); + } else if (iron.RenderPath.isVRSimulateMode()) { + var ipd_offset = 0.032 * 35.0; + var rightVec = camera.rightWorld(); + var centerX = camera.transform.worldx(); + var centerY = camera.transform.worldy(); + var centerZ = camera.transform.worldz(); + helpVec.set( + centerX + rightVec.x * ipd_offset, + centerY + rightVec.y * ipd_offset, + centerZ + rightVec.z * ipd_offset + ); + } else { + helpVec.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz()); + } + v = helpVec; + } + case "_eyeLookLeft": { + var vr = kha.vr.VrInterface.instance; + if (vr != null && vr.IsPresenting()) { + var leftViewMatrix = vr.GetViewMatrix(0); + var invLeft = leftViewMatrix.inverse(); + helpVec.set(-invLeft._20, -invLeft._21, -invLeft._22); + helpVec.normalize(); + } else { + helpVec = camera.lookWorld().normalize(); + } + v = helpVec; + } + case "_eyeLookRight": { + var vr = kha.vr.VrInterface.instance; + if (vr != null && vr.IsPresenting()) { + var rightViewMatrix = vr.GetViewMatrix(1); + var invRight = rightViewMatrix.inverse(); + helpVec.set(-invRight._20, -invRight._21, -invRight._22); + helpVec.normalize(); + } else { + helpVec = camera.lookWorld().normalize(); + } + v = helpVec; + } + #end case "_backgroundCol": { if (camera.data.raw.clear_color != null) helpVec.set(camera.data.raw.clear_color[0], camera.data.raw.clear_color[1], camera.data.raw.clear_color[2]); v = helpVec; @@ -1161,7 +1361,19 @@ class Uniforms { #end // lnx_clusters #if lnx_morph_target case "_morphWeights": { - fa = cast(object, MeshObject).morphTarget.morphWeights; + var morphTarget = cast(object, MeshObject).morphTarget; + morphTarget.flushBatchedUpdates(); + if (forceUploadMorphWeights) { + fa = morphTarget.morphWeights; + } + else { + if (morphTarget.isDirty) { + fa = morphTarget.morphWeights; + } + else { + return; + } + } } #end } @@ -1175,6 +1387,12 @@ class Uniforms { if (fa == null) return; g.setFloats(location, fa); + + #if lnx_morph_target + if (c.link == "_morphWeights") { + cast(object, MeshObject).morphTarget.markClean(); + } + #end } else if (c.type == "int") { var i: Null = null; @@ -1203,6 +1421,7 @@ class Uniforms { if (materialContext.raw.bind_constants != null) { for (i in 0...materialContext.raw.bind_constants.length) { var matc = materialContext.raw.bind_constants[i]; + if (matc == null) continue; var pos = -1; for (i in 0...context.raw.constants.length) { if (context.raw.constants[i].name == matc.name) { diff --git a/leenkx/Sources/iron/system/Time.hx b/leenkx/Sources/iron/system/Time.hx index 0ff7dbd..53f8ceb 100644 --- a/leenkx/Sources/iron/system/Time.hx +++ b/leenkx/Sources/iron/system/Time.hx @@ -3,6 +3,14 @@ package iron.system; class Time { public static var scale = 1.0; + // TODO: VR Frame Time Override - used to sync physics with VR headset refresh rate + #if lnx_vr + public static var vrFrameTime: Float = -1.0; // VR frame time in seconds (-1 = not in VR) + static var lastVRFrameTime: Float = 0.0; + static var vrFrameCount: Int = 0; + static var normalModeLogged: Bool = false; + #end + static var frequency: Null = null; static function initFrequency() { frequency = kha.Display.primary != null ? kha.Display.primary.frequency : 60; @@ -47,6 +55,24 @@ class Time { } public static function update() { + #if lnx_vr + // TODO: use VR frame time when in VR present mode to sync physics with headset refresh + if (vrFrameTime >= 0.0) { + if (lastVRFrameTime > 0.0) { + _delta = vrFrameTime - lastVRFrameTime; + } else { + _delta = 1.0 / 90.0; // Default to 90Hz for first VR frame + } + lastVRFrameTime = vrFrameTime; + + return; + } else { + if (!normalModeLogged) { + normalModeLogged = true; + } + } + #end + _delta = realTime() - lastTime; lastTime = realTime(); } diff --git a/leenkx/Sources/iron/system/VRController.hx b/leenkx/Sources/iron/system/VRController.hx new file mode 100644 index 0000000..c3d15bf --- /dev/null +++ b/leenkx/Sources/iron/system/VRController.hx @@ -0,0 +1,138 @@ +package iron.system; + +#if lnx_vr +import iron.math.Vec4; +import iron.math.Quat; + +class VRController { + + public static var leftHandPosition: Vec4 = new Vec4(); + public static var leftHandRotation: Quat = new Quat(); + public static var rightHandPosition: Vec4 = new Vec4(); + public static var rightHandRotation: Quat = new Quat(); + + public static var leftHandActive: Bool = false; + public static var rightHandActive: Bool = false; + + public static var leftThumbstickX: Float = 0.0; + public static var leftThumbstickY: Float = 0.0; + public static var rightThumbstickX: Float = 0.0; + public static var rightThumbstickY: Float = 0.0; + + public static var leftTrigger: Float = 0.0; + public static var rightTrigger: Float = 0.0; + public static var leftGrip: Float = 0.0; + public static var rightGrip: Float = 0.0; + + public static var leftButtonX: Bool = false; + public static var leftButtonY: Bool = false; + public static var rightButtonA: Bool = false; + public static var rightButtonB: Bool = false; + + public static var debugLog:Bool = false; + + public static function enableDebug() { + debugLog = true; + } + + public static function disableDebug() { + debugLog = false; + } + + public static function updatePoses() { + var vr: kha.js.vr.VrInterface = cast kha.vr.VrInterface.instance; + if (vr == null || !vr.IsPresenting()) { + if (debugLog) trace("[VRController] Not presenting or VR null"); + leftHandActive = false; + rightHandActive = false; + return; + } + + untyped window._vrControllerFrame = (untyped window._vrControllerFrame || 0) + 1; + + leftHandActive = false; + rightHandActive = false; + + leftButtonX = false; + leftButtonY = false; + rightButtonA = false; + rightButtonB = false; + + var refSpace = untyped vr.xrRefSpace; + if (vr.currentInputSources == null || vr.currentFrame == null || refSpace == null) { + return; + } + + var inputSources = vr.currentInputSources; + var frame = vr.currentFrame; + + var sourceCount:Int = untyped inputSources.length; + + for (i in 0...sourceCount) { + var inputSource = untyped inputSources[i]; + if (inputSource == null) continue; + + var handedness = untyped inputSource.handedness; // "left", "right", or "none" + + var gripSpace = untyped inputSource.gripSpace; + var targetRaySpace = untyped inputSource.targetRaySpace; + // use targetRaySpace first laser/pointer and fall back to gripSpace + var space = (targetRaySpace != null) ? targetRaySpace : gripSpace; + + if (space == null) { + continue; + } + + var pose = untyped frame.getPose(space, refSpace); + if (pose == null || pose.transform == null) { + continue; + } + + var transform = pose.transform; + var pos = transform.position; + var orient = transform.orientation; + + if (handedness == "left") { + leftHandPosition.set(pos.x, pos.y, pos.z); + leftHandRotation.set(orient.x, orient.y, orient.z, orient.w); + leftHandActive = true; + + var gamepad = untyped inputSource.gamepad; + if (gamepad != null) { + // [0]=thumbstickX [1]=thumbstickY [2]=touchpadX [3]=touchpadY + if (gamepad.axes != null && gamepad.axes.length >= 2) { + leftThumbstickX = gamepad.axes[0]; + leftThumbstickY = gamepad.axes[1]; + } + // [0]=trigger [1]=grip [4]=X [5]=Y + if (gamepad.buttons != null) { + if (gamepad.buttons.length > 0) leftTrigger = gamepad.buttons[0].value; + if (gamepad.buttons.length > 1) leftGrip = gamepad.buttons[1].value; + if (gamepad.buttons.length > 4) leftButtonX = gamepad.buttons[4].pressed; + if (gamepad.buttons.length > 5) leftButtonY = gamepad.buttons[5].pressed; + } + } + } + else if (handedness == "right") { + rightHandPosition.set(pos.x, pos.y, pos.z); + rightHandRotation.set(orient.x, orient.y, orient.z, orient.w); + rightHandActive = true; + + var gamepad = untyped inputSource.gamepad; + if (gamepad != null) { + if (gamepad.axes != null && gamepad.axes.length >= 2) { + rightThumbstickX = gamepad.axes[0]; + rightThumbstickY = gamepad.axes[1]; + } + if (gamepad.buttons != null) { + if (gamepad.buttons.length > 0) rightTrigger = gamepad.buttons[0].value; + if (gamepad.buttons.length > 1) rightGrip = gamepad.buttons[1].value; + if (gamepad.buttons.length > 4) rightButtonA = gamepad.buttons[4].pressed; + if (gamepad.buttons.length > 5) rightButtonB = gamepad.buttons[5].pressed; + } + } + } + } + } +} +#end