diff --git a/leenkx/Sources/leenkx/data/Config.hx b/leenkx/Sources/leenkx/data/Config.hx index 569f369..63be2bf 100644 --- a/leenkx/Sources/leenkx/data/Config.hx +++ b/leenkx/Sources/leenkx/data/Config.hx @@ -44,6 +44,7 @@ typedef TConfig = { @:optional var rp_supersample: Null; @:optional var rp_shadowmap_cube: Null; // size @:optional var rp_shadowmap_cascade: Null; // size for single cascade + @:optional var rp_ssao: Null; @:optional var rp_ssgi: Null; @:optional var rp_ssr: Null; @:optional var rp_ssrefr: Null; diff --git a/leenkx/Sources/leenkx/network/Connect.hx b/leenkx/Sources/leenkx/network/Connect.hx index 5a48697..7196e77 100644 --- a/leenkx/Sources/leenkx/network/Connect.hx +++ b/leenkx/Sources/leenkx/network/Connect.hx @@ -1,11 +1,13 @@ package leenkx.network; -#if sys +#if (sys || kha_krom) import leenkx.network.WebSocketServer; import leenkx.network.WebSocketSecureServer; +import leenkx.network.SocketImpl; +#end +#if sys import sys.ssl.Key; import sys.ssl.Certificate; -import leenkx.network.SocketImpl; #end import leenkx.network.WebSocket; import leenkx.network.Types; @@ -115,7 +117,7 @@ class Host extends Connect { public static var onErrorEvent: String = "Host.onError"; public static var onCloseEvent: String = "Host.onClose"; public static var object: Object = null; - #if sys + #if (sys || kha_krom) public static var connections:Map> = []; #else public static var connections = null; @@ -131,14 +133,15 @@ class Host extends Connect { object = net_object; net_Url = "ws://" + net_Domain + ":" + net_Port; - #if sys - if (connections[net_Url] != null) return; - connections[net_Url] = new WebSocketServer(net_Domain, net_Port, net_Max); + #if (sys || kha_krom) + if (connections[net_Url] == null) { + connections[net_Url] = new WebSocketServer(net_Domain, net_Port, net_Max); + } #end } } -#if sys +#if (sys || kha_krom) class HostHandler extends WebSocketHandler { public function new(s: SocketImpl) { @@ -217,7 +220,7 @@ class SecureHost extends Connect { public static var onCloseEvent: String = "SecureHost.onClose"; public static var object: Object = null; public static var net_Url: String; - #if sys + #if (sys || kha_krom) public static var connections:Map> = []; #else public static var connections = null; @@ -235,13 +238,18 @@ class SecureHost extends Connect { #if sys var cert = Certificate.loadFile(net_Cert); var key = Key.loadFile(net_Key); - if (connections[net_Url] != null) return; - connections[net_Url] = new WebSocketSecureServer(net_Domain, net_Port, cert, key, cert, net_Max); + if (connections[net_Url] == null) { + connections[net_Url] = new WebSocketSecureServer(net_Domain, net_Port, cert, key, cert, net_Max); + } + #elseif kha_krom + if (connections[net_Url] == null) { + connections[net_Url] = new WebSocketSecureServer(net_Domain, net_Port, net_Cert, net_Key, net_Cert, net_Max); + } #end } } -#if sys +#if (sys || kha_krom) class SecureHostHandler extends WebSocketHandler { public function new(s: SocketImpl) { @@ -313,3 +321,4 @@ class SecureHostHandler extends WebSocketHandler { } } #end + diff --git a/leenkx/Sources/leenkx/network/Leenkx.hx b/leenkx/Sources/leenkx/network/Leenkx.hx index 39d9a91..de38fc1 100644 --- a/leenkx/Sources/leenkx/network/Leenkx.hx +++ b/leenkx/Sources/leenkx/network/Leenkx.hx @@ -2,7 +2,6 @@ package leenkx.network; import leenkx.network.Types; import haxe.io.Bytes; -import js.Browser; import iron.object.Object; import leenkx.system.Event; import leenkx.network.Buffer; @@ -30,11 +29,13 @@ class Leenkx { public static var onAnnounceEvent: String = "Leenkx.onAnnounce"; public static var onTorrentDoneEvent: String = "Leenkx.onTorrentDone"; public static var connections:Map = []; + #if js public static var peers:js.lib.Map = new js.lib.Map(); public static var data:js.lib.Map = new js.lib.Map(); public static var id:js.lib.Map = new js.lib.Map(); public static var torrent:js.lib.Map = new js.lib.Map(); - public static var file:js.lib.Map = new js.lib.Map(); + public static var file:js.lib.Map = new js.lib.Map(); + #end public static var lxNew:Void->Void; public function new(net_Url: String, net_object: Object) { @@ -53,10 +54,6 @@ class Leenkx { } if (object != null) { - //var script = "const scope = '/';const sw = navigator.serviceWorker.register(`LeenkxFS.js`, { scope });"; - //js.Syntax.code('(1, eval)({0})', script); - - final loadEvent = Event.get(Leenkx.onLoadEvent); final openEvent = Event.get(Leenkx.onOpenEvent); final messageEvent = Event.get(Leenkx.onMessageEvent); @@ -77,197 +74,204 @@ class Leenkx { final announceEvent = Event.get(Leenkx.onAnnounceEvent); final torrentDoneEvent = Event.get(Leenkx.onTorrentDoneEvent); + Leenkx.connections[net_Url].onopen = function() { + if (openEvent != null) { + for (e in openEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onmessage = function() { + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onerror = function() { + if (errorEvent != null) { + for (e in errorEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onclose = function() { + if (closeEvent != null) { + for (e in closeEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onseen = function() { + if (seenEvent != null) { + for (e in seenEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onserver = function() { + if (serverEvent != null) { + for (e in serverEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onconnections = function() { + if (connectionsEvent != null) { + for (e in connectionsEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onping = function() { + if (pingEvent != null) { + for (e in pingEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onleft = function() { + if (leftEvent != null) { + for (e in leftEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].ontimeout = function() { + if (timeoutEvent != null) { + for (e in timeoutEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onrpc = function() { + if (rpcEvent != null) { + for (e in rpcEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onrpcresponse = function() { + if (rpcresponseEvent != null) { + for (e in rpcresponseEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onwireleft = function() { + if (wireleftEvent != null) { + for (e in wireleftEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onwireseen = function() { + if (wireseenEvent != null) { + for (e in wireseenEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].ontorrent = function() { + if (torrentEvent != null) { + for (e in torrentEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].ontracker = function() { + if (trackerEvent != null) { + for (e in trackerEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onannounce = function() { + if (announceEvent != null) { + for (e in announceEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].onload = function() { + if (loadEvent != null) { + for (e in loadEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; + Leenkx.connections[net_Url].ontorrentdone = function() { + if (torrentDoneEvent != null) { + for (e in torrentDoneEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + }; - var lnkx = Browser.document.createScriptElement(); - lnkx.type = "text/javascript"; - lnkx.src = "Leenkx.js"; - lnkx.onload = function() { - - js.Syntax.code('(1, eval)({0})', 'lxNew =function(url){ var cx = new Leenkx(url); return cx;}'); - - Leenkx.connections[net_Url].onopen = function() { - if (openEvent != null) { - for (e in openEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } + #if js + var lnxjs:Dynamic = js.Lib.global; + var connectionUrl = net_Url; + + if (lnxjs.Leenkx != null) { + if (lnxjs.lnxNew == null) { + js.Syntax.code('globalThis.lnxNew = function(url) { return new Leenkx(url); }'); } - Leenkx.connections[net_Url].onmessage = function() { - if (messageEvent != null) { - for (e in messageEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } + Leenkx.connections[connectionUrl].onload(); + } else { + kha.Assets.loadBlobFromPath("Leenkx.js", function(b: kha.Blob) { + if (b != null) { + js.Syntax.code("(1,eval)({0})", b.toString()); + if (lnxjs.Leenkx != null && lnxjs.lnxNew == null) { + js.Syntax.code('globalThis.lnxNew = function(url) { return new Leenkx(url); }'); } + } else { + trace("Warning: Leenkx.js blob is null - file may not be in assets"); } - } - Leenkx.connections[net_Url].onerror = function() { - if (errorEvent != null) { - for (e in errorEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onclose = function() { - if (closeEvent != null) { - for (e in closeEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onseen = function() { - if (seenEvent != null) { - for (e in seenEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onserver = function() { - if (serverEvent != null) { - for (e in serverEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onconnections = function() { - if (connectionsEvent != null) { - for (e in connectionsEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onping = function() { - if (pingEvent != null) { - for (e in pingEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onleft = function() { - if (leftEvent != null) { - for (e in leftEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].ontimeout = function() { - if (timeoutEvent != null) { - for (e in timeoutEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onrpc = function() { - if (rpcEvent != null) { - for (e in rpcEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onrpcresponse = function() { - if (rpcresponseEvent != null) { - for (e in rpcresponseEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onwireleft = function() { - if (wireleftEvent != null) { - for (e in wireleftEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onwireseen = function() { - if (wireseenEvent != null) { - for (e in wireseenEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].ontorrent = function() { - if (torrentEvent != null) { - for (e in torrentEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].ontracker = function() { - if (trackerEvent != null) { - for (e in trackerEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onannounce = function() { - if (announceEvent != null) { - for (e in announceEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].onload = function() { - if (loadEvent != null) { - for (e in loadEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - Leenkx.connections[net_Url].ontorrentdone = function() { - if (torrentDoneEvent != null) { - for (e in torrentDoneEvent) { - if (e.mask == object.uid) { - e.onEvent(); - } - } - } - } - - Leenkx.connections[net_Url].onload(); - //var script = 'leenkx.network.Leenkx.connections.h["' + net_Url + '"].onload();'; - //js.Syntax.code('(1, eval)({0})', script); - - } - - lnkx.onerror = function(error, i) { - trace("ERROR - " + error + " | " + i); - } - - Browser.document.head.appendChild(lnkx); + Leenkx.connections[connectionUrl].onload(); + }, function(err: kha.AssetError) { + trace("ERROR loading Leenkx.js: " + err.url + " - " + err.error); + Leenkx.connections[connectionUrl].onload(); + }); + } + #end } } } diff --git a/leenkx/Sources/leenkx/network/Log.hx b/leenkx/Sources/leenkx/network/Log.hx index 2cdf6ee..44e1f01 100644 --- a/leenkx/Sources/leenkx/network/Log.hx +++ b/leenkx/Sources/leenkx/network/Log.hx @@ -7,8 +7,8 @@ class Log { public static var mask:Int = 0; - #if sys - public static var logFn:Dynamic->Void = Sys.println; + #if (sys || kha_krom) + public static var logFn:Dynamic->Void = function(data:Dynamic) { trace(data); }; #elseif js public static var logFn:Dynamic->Void = js.html.Console.log; #end diff --git a/leenkx/Sources/leenkx/network/SecureSocketImpl.hx b/leenkx/Sources/leenkx/network/SecureSocketImpl.hx index ed95c87..74b5ee4 100644 --- a/leenkx/Sources/leenkx/network/SecureSocketImpl.hx +++ b/leenkx/Sources/leenkx/network/SecureSocketImpl.hx @@ -1,3 +1,7 @@ package leenkx.network; +#if kha_krom +typedef SecureSocketImpl = leenkx.network.krom.KromSecureSocket; +#else typedef SecureSocketImpl = sys.ssl.Socket; +#end diff --git a/leenkx/Sources/leenkx/network/SocketImpl.hx b/leenkx/Sources/leenkx/network/SocketImpl.hx index 28cecae..5fb332c 100644 --- a/leenkx/Sources/leenkx/network/SocketImpl.hx +++ b/leenkx/Sources/leenkx/network/SocketImpl.hx @@ -12,6 +12,10 @@ typedef SocketImpl = leenkx.network.cs.NonBlockingSocket; typedef SocketImpl = leenkx.network.nodejs.NodeSocket; +#elseif kha_krom + +typedef SocketImpl = leenkx.network.krom.KromSocket; + #else typedef SocketImpl = sys.net.Socket; diff --git a/leenkx/Sources/leenkx/network/WebSocket.hx b/leenkx/Sources/leenkx/network/WebSocket.hx index 3905a14..c3438f5 100644 --- a/leenkx/Sources/leenkx/network/WebSocket.hx +++ b/leenkx/Sources/leenkx/network/WebSocket.hx @@ -2,7 +2,128 @@ package leenkx.network; import leenkx.network.Types; -#if js +#if kha_krom + +import haxe.io.Bytes; + +#if (haxe_ver < 4) +typedef JsBuffer = js.html.ArrayBuffer; +#else +typedef JsBuffer = js.lib.ArrayBuffer; +#end + +@:native("WebSocket") +extern class NativeWebSocket { + var binaryType:String; + var onopen:Dynamic; + var onclose:Dynamic; + var onerror:Dynamic; + var onmessage:Dynamic; + var readyState:Int; + function new(url:String):Void; + function close(?code:Int, ?reason:String):Void; + function send(data:Dynamic):Void; +} + +class WebSocket { + public var _protocol:String; + public var _host:String; + public var _port:Int = 0; + public var _path:String; + private var _url:String; + + private var _ws:NativeWebSocket = null; + + public var binaryType:Dynamic; + + public var onopen:Void->Void; + public var onclose:Void->Void; + public var onerror:Dynamic->Void; + public var onmessage:MessageType->Void; + + public function new(url:String, immediateOpen:Bool = true) { + _url = url; + parseUrl(url); + if (immediateOpen) { + open(); + } + } + + private function parseUrl(url:String):Void { + var urlArr = url.split(":"); + if (urlArr.length < 2) return; + _protocol = urlArr[0]; + var hostPart = urlArr[1]; + if (hostPart.substr(0, 2) == "//") hostPart = hostPart.substr(2); + _host = hostPart; + if (urlArr.length >= 3) { + var portPathPart = urlArr[2]; + var slashIndex = portPathPart.indexOf("/"); + if (slashIndex >= 0) { + _port = Std.parseInt(portPathPart.substr(0, slashIndex)); + _path = portPathPart.substr(slashIndex); + } else { + _port = Std.parseInt(portPathPart); + _path = "/"; + } + } else { + _port = (_protocol == "wss") ? 443 : 80; + _path = "/"; + } + if (_port == null || _port == 0) _port = (_protocol == "wss") ? 443 : 80; + } + + public function open():Void { + if (_ws != null) { + throw "Socket already connected"; + } + _ws = new NativeWebSocket(_url); + _ws.binaryType = "arraybuffer"; + + _ws.onopen = function() { + if (onopen != null) onopen(); + }; + _ws.onclose = function() { + if (onclose != null) onclose(); + }; + _ws.onerror = function(err:Dynamic) { + if (onerror != null) onerror(err); + }; + _ws.onmessage = function(event:Dynamic) { + if (onmessage != null) { + if (Std.isOfType(event.data, JsBuffer)) { + var buffer = new Buffer(); + buffer.writeBytes(Bytes.ofData(event.data)); + onmessage(MessageType.BytesMessage(buffer)); + } else { + onmessage(MessageType.StrMessage(event.data)); + } + } + }; + } + + public function close():Void { + if (_ws != null) { + _ws.close(); + _ws = null; + } + } + + public function send(msg:Dynamic):Void { + if (_ws == null) return; + if (Std.isOfType(msg, Bytes)) { + var bytes:Bytes = cast msg; + _ws.send(bytes.getData()); + } else if (Std.isOfType(msg, Buffer)) { + var buffer:Buffer = cast msg; + _ws.send(buffer.readAllAvailableBytes().getData()); + } else { + _ws.send(msg); + } + } +} + +#elseif js import haxe.Constraints.Function; import haxe.io.Bytes; diff --git a/leenkx/Sources/leenkx/network/WebSocketHandler.hx b/leenkx/Sources/leenkx/network/WebSocketHandler.hx index ccb2842..11e8f2f 100644 --- a/leenkx/Sources/leenkx/network/WebSocketHandler.hx +++ b/leenkx/Sources/leenkx/network/WebSocketHandler.hx @@ -7,13 +7,22 @@ class WebSocketHandler extends Handler { public function new(socket:SocketImpl) { super(socket); + #if kha_krom + _creationTime = kha.Scheduler.time(); + #else _creationTime = Sys.time(); + #end _socket.setBlocking(false); Log.debug('New socket handler', id); } public override function handle() { - if (this.state == State.Handshake && Sys.time() - _creationTime > (MAX_WAIT_TIME / 1000)) { + #if kha_krom + var currentTime = kha.Scheduler.time(); + #else + var currentTime = Sys.time(); + #end + if (this.state == State.Handshake && currentTime - _creationTime > (MAX_WAIT_TIME / 1000)) { Log.info('No handshake detected in ${MAX_WAIT_TIME}ms, closing connection', id); this.close(); return; diff --git a/leenkx/Sources/leenkx/network/WebSocketSecureServer.hx b/leenkx/Sources/leenkx/network/WebSocketSecureServer.hx index b059849..c597118 100644 --- a/leenkx/Sources/leenkx/network/WebSocketSecureServer.hx +++ b/leenkx/Sources/leenkx/network/WebSocketSecureServer.hx @@ -2,8 +2,10 @@ package leenkx.network; import haxe.Constraints; +#if !kha_krom import sys.ssl.Key; import sys.ssl.Certificate; +#end @:generic class WebSocketSecureServer @@ -14,11 +16,21 @@ class WebSocketSecureServer #end extends WebSocketServer { + #if kha_krom + private var _cert:Dynamic; + private var _key:Dynamic; + private var _caChain:Dynamic; + #else private var _cert:Certificate; private var _key:Key; private var _caChain:Certificate; + #end + #if kha_krom + public function new(host:String, port:Int, cert:Dynamic, key:Dynamic, caChain:Dynamic, maxConnections:Int = 1) { + #else public function new(host:String, port:Int, cert:Certificate, key:Key, caChain:Certificate, maxConnections:Int = 1) { + #end super(host, port, maxConnections); _cert=cert; diff --git a/leenkx/Sources/leenkx/network/WebSocketServer.hx b/leenkx/Sources/leenkx/network/WebSocketServer.hx index cb2a839..11976ee 100644 --- a/leenkx/Sources/leenkx/network/WebSocketServer.hx +++ b/leenkx/Sources/leenkx/network/WebSocketServer.hx @@ -3,6 +3,9 @@ package leenkx.network; import haxe.Constraints; import haxe.MainLoop; import haxe.io.Error; +#if kha_krom +import leenkx.network.krom.KromSocket.KromHost; +#end @:generic class WebSocketServer @@ -51,11 +54,20 @@ class WebSocketServer _stopServer = false; _serverSocket = createSocket(); _serverSocket.setBlocking(false); + #if kha_krom + _serverSocket.bind(new KromHost(_host), _port); + #else _serverSocket.bind(new sys.net.Host(_host), _port); + #end _serverSocket.listen(_maxConnections); Log.info('Starting server - ${_host}:${_port} (maxConnections: ${_maxConnections})'); - #if cs + #if kha_krom + kha.Scheduler.addTimeTask(function() { + tick(); + }, 0, sleepAmount); + + #elseif cs while (true) { var continueLoop = tick(); if (continueLoop == false) { diff --git a/leenkx/Sources/leenkx/network/krom/KromSecureSocket.hx b/leenkx/Sources/leenkx/network/krom/KromSecureSocket.hx new file mode 100644 index 0000000..62b09a6 --- /dev/null +++ b/leenkx/Sources/leenkx/network/krom/KromSecureSocket.hx @@ -0,0 +1,62 @@ +package leenkx.network.krom; + +import haxe.io.Bytes; +import haxe.io.BytesOutput; +import haxe.io.Error; +import leenkx.network.krom.KromSocket.KromHost; + +@:native("krom_socket_enable_ssl") extern function krom_socket_enable_ssl(id:Int):Bool; + +class KromSecureSocket extends KromSocket { + private var _sslEnabled:Bool = false; + private var _hostname:String = null; + private var _certPath:String = null; + private var _keyPath:String = null; + private var _caPath:String = null; + public var verifyCert:Bool = true; + + public function new() { + super(); + } + + public function setHostname(hostname:String):Void { + _hostname = hostname; + } + + public function setCA(ca:Dynamic):Void { + if (ca != null) { + _caPath = Std.string(ca); + } + } + + public function setCertificate(cert:Dynamic, key:Dynamic):Void { + if (cert != null) { + _certPath = Std.string(cert); + } + if (key != null) { + _keyPath = Std.string(key); + } + } + + public function enableSSL():Bool { + if (getSocketId() >= 0 && !_sslEnabled) { + var result = krom_socket_enable_ssl(getSocketId()); + if (result) { + _sslEnabled = true; + } else { + trace("SecureSocket: Failed to enable SSL for socket " + getSocketId()); + } + return result; + } + return _sslEnabled; + } + + override public function connect(host:KromHost, port:Int):Void { + super.connect(host, port); + enableSSL(); + } + + public function isSSLEnabled():Bool { + return _sslEnabled; + } +} diff --git a/leenkx/Sources/leenkx/network/krom/KromSocket.hx b/leenkx/Sources/leenkx/network/krom/KromSocket.hx new file mode 100644 index 0000000..e7aeada --- /dev/null +++ b/leenkx/Sources/leenkx/network/krom/KromSocket.hx @@ -0,0 +1,249 @@ +package leenkx.network.krom; + +import haxe.io.Bytes; +import haxe.io.BytesInput; +import haxe.io.BytesOutput; +import haxe.io.Error; + +#if (haxe_ver < 4) +typedef JsBuffer = js.html.ArrayBuffer; +#else +typedef JsBuffer = js.lib.ArrayBuffer; +#end + +@:native("krom_socket_create") extern function krom_socket_create():Int; +@:native("krom_socket_close") extern function krom_socket_close(id:Int):Void; +@:native("krom_socket_bind") extern function krom_socket_bind(id:Int, addr:String, port:Int):Bool; +@:native("krom_socket_listen") extern function krom_socket_listen(id:Int, backlog:Int):Bool; +@:native("krom_socket_accept") extern function krom_socket_accept(id:Int):Int; +@:native("krom_socket_connect") extern function krom_socket_connect(id:Int, host:String, port:Int):Bool; +@:native("krom_socket_send") extern function krom_socket_send(id:Int, data:JsBuffer):Int; +@:native("krom_socket_recv") extern function krom_socket_recv(id:Int, maxLen:Int):Dynamic; +@:native("krom_socket_set_blocking") extern function krom_socket_set_blocking(id:Int, blocking:Bool):Void; +@:native("krom_socket_is_connected") extern function krom_socket_is_connected(id:Int):Bool; + +class KromSocket { + private var _socketId:Int = -1; + private var _host:KromHost = null; + private var _port:Int = 0; + private var _blocking:Bool = true; + + public var input:KromSocketInput = null; + public var output:KromSocketOutput = null; + + private static var _connections:Array = []; + private var _newConnections:Array = []; + + public function new() { + _socketId = krom_socket_create(); + if (_socketId >= 0) { + input = new KromSocketInput(this); + output = new KromSocketOutput(this); + } + } + + private function setSocketId(id:Int):Void { + _socketId = id; + if (_socketId >= 0) { + input = new KromSocketInput(this); + output = new KromSocketOutput(this); + } + } + + public function getSocketId():Int { + return _socketId; + } + + public function bind(host:KromHost, port:Int):Void { + _host = host; + _port = port; + if (_socketId >= 0) { + if (!krom_socket_bind(_socketId, host.host, port)) { + trace("Failed to bind to " + host.host + ":" + port); + } + } + } + + public function listen(connections:Int):Void { + if (_host == null) { + throw "You must bind the Socket to an address!"; + } + if (_socketId >= 0) { + if (!krom_socket_listen(_socketId, connections)) { + trace("Failed to listen"); + } + } + } + + public function accept():KromSocket { + if (_socketId < 0) { + throw "Blocking"; + } + + var clientId = krom_socket_accept(_socketId); + if (clientId < 0) { + throw "Blocking"; + } + + var clientSocket = new KromSocket(); + clientSocket.setSocketId(clientId); + _connections.push(clientSocket); + return clientSocket; + } + + public function connect(host:KromHost, port:Int):Void { + if (_socketId >= 0) { + if (!krom_socket_connect(_socketId, host.host, port)) { + throw "Connection failed to " + host.host + ":" + port; + } + } + } + + public function setBlocking(blocking:Bool):Void { + _blocking = blocking; + if (_socketId >= 0) { + krom_socket_set_blocking(_socketId, blocking); + } + } + + public function setTimeout(timeout:Int):Void { + // TODO: Re-investigate timeout handling + } + + public function peer():{host:KromHost, port:Int} { + return {host: _host != null ? _host : new KromHost("0.0.0.0"), port: _port}; + } + + public function host():{host:KromHost, port:Int} { + return {host: _host != null ? _host : new KromHost("0.0.0.0"), port: _port}; + } + + public function close():Void { + if (_socketId >= 0) { + krom_socket_close(_socketId); + _socketId = -1; + } + _connections.remove(this); + } + + public function sendRaw(data:Bytes):Int { + var arrayBuffer:JsBuffer = data.getData(); + var exactBuffer:JsBuffer = untyped arrayBuffer.slice(0, data.length); + return krom_socket_send(_socketId, exactBuffer); + } + + // throws "ConnectionClosed" if peer disconnected + public function recvRaw(maxLength:Int):Bytes { + if (_socketId < 0) return null; + var result:Dynamic = krom_socket_recv(_socketId, maxLength); + // V8 returns ArrayBuffer data, null would-block, or -2 connection closed + if (result == -2) { + _socketId = -1; + throw "ConnectionClosed"; + } + if (result == null) return null; + return Bytes.ofData(result); + } + + public function isConnected():Bool { + if (_socketId < 0) return false; + return krom_socket_is_connected(_socketId); + } + + public static function select(read:Array, write:Array, others:Array, ?timeout:Float):{read:Array, write:Array, others:Array} { + var readable:Array = []; + + if (read != null) { + for (sock in read) { + if (sock._socketId >= 0) { + readable.push(sock); + } + } + } + + if (readable.length == 0) { + return null; + } + + return { + read: readable, + write: write, + others: others + }; + } +} + +class KromSocketInput { + private var _socket:KromSocket; + public var hasData:Bool = false; + private var _buffer:Bytes = null; + private var _bufferPos:Int = 0; + + public function new(socket:KromSocket) { + _socket = socket; + } + + public function readBytes(buf:Bytes, pos:Int, len:Int):Int { + var data = _socket.recvRaw(len); + if (data == null || data.length == 0) { + hasData = false; + throw Error.Blocked; + } + hasData = true; + var toRead = data.length < len ? data.length : len; + buf.blit(pos, data, 0, toRead); + return toRead; + } + + public function read(nbytes:Int):Bytes { + var buf = Bytes.alloc(nbytes); + var read = readBytes(buf, 0, nbytes); + if (read < nbytes) { + return buf.sub(0, read); + } + return buf; + } +} + +class KromSocketOutput { + private var _socket:KromSocket; + private var _buffer:BytesOutput; + + public function new(socket:KromSocket) { + _socket = socket; + _buffer = new BytesOutput(); + } + + public function write(data:Bytes):Void { + _buffer.write(data); + } + + public function writeBytes(buf:Bytes, pos:Int, len:Int):Int { + _buffer.writeBytes(buf, pos, len); + return len; + } + + public function flush():Void { + var data = _buffer.getBytes(); + if (data.length > 0) { + _socket.sendRaw(data); + _buffer = new BytesOutput(); + } + } +} + +class KromHost { + public var host:String; + + public function new(hostname:String) { + host = hostname; + } + + public static function resolve(hostname:String):String { + return hostname; + } + + public function toString():String { + return host; + } +} diff --git a/leenkx/Sources/leenkx/object/AnimationExtension.hx b/leenkx/Sources/leenkx/object/AnimationExtension.hx index 7c0401b..1e1de3f 100644 --- a/leenkx/Sources/leenkx/object/AnimationExtension.hx +++ b/leenkx/Sources/leenkx/object/AnimationExtension.hx @@ -13,11 +13,15 @@ import iron.math.Vec2; import haxe.ds.Vector; import iron.object.Object; import iron.object.Animation; +import iron.object.Animation.ActionSampler; +#if lnx_skin import iron.object.BoneAnimation; +#end import iron.object.ObjectAnimation; class AnimationExtension { + #if lnx_skin public static function solveIKBlend(boneAnimation: BoneAnimation, actionMats: Array, effector: TObj, goal: Vec4, precision = 0.01, maxIterations = 100, chainLenght = 100, pole: Vec4 = null, rollAngle = 0.0, influence = 0.0, layerMask: Null = null, threshold: FastFloat = 0.1) { var matsBlend = boneAnimation.initMatsEmpty(); @@ -31,7 +35,9 @@ class AnimationExtension { boneAnimation.solveIK(actionMats, effector, goal, precision, maxIterations, chainLenght, pole, rollAngle); boneAnimation.blendAction(matsBlend, actionMats, actionMats, influence, layerMask, threshold); } + #end + #if lnx_skin public static function solveTwoBoneIKBlend(boneAnimation: BoneAnimation, actionMats: Array, effector: TObj, goal: Vec4, pole: Vec4 = null, rollAngle = 0.0, influence = 0.0, layerMask: Null = null, threshold: FastFloat = 0.1) { var matsBlend = boneAnimation.initMatsEmpty(); @@ -45,6 +51,7 @@ class AnimationExtension { boneAnimation.solveTwoBoneIK(actionMats, effector, goal, pole, rollAngle); boneAnimation.blendAction(matsBlend, actionMats, actionMats, influence, layerMask, threshold); } + #end static inline function sortWeights(vecs: Array, sampleVec: Vec2): Map { var weightIndex: Array = []; @@ -113,7 +120,9 @@ class AnimationExtension { class OneShotOperator { + #if lnx_skin var boneAnimation: BoneAnimation; + #end var objectAnimation: ObjectAnimation; var isArmature: Bool; var oneShotAction: ActionSampler; @@ -136,12 +145,15 @@ class OneShotOperator { var animation = animation; this.oneShotAction = oneShotAction; + #if lnx_skin if(Std.isOfType(animation, BoneAnimation)) { boneAnimation = cast animation; tempMatsBone = boneAnimation.initMatsEmpty(); this.isArmature = true; } - else { + else + #end + { objectAnimation = cast animation; tempMatsObject = objectAnimation.initTransformMap(); this.isArmature = false; @@ -150,10 +162,13 @@ class OneShotOperator { } function initOneShot() { + #if lnx_skin if(isArmature) { totalFrames = boneAnimation.getTotalFrames(oneShotAction) - 1; } - else { + else + #end + { totalFrames = objectAnimation.getTotalFrames(oneShotAction) - 1; } blendFactor = 0.0; @@ -257,7 +272,9 @@ class OneShotOperator { class SwitchActionOperator { + #if lnx_skin var boneAnimation: BoneAnimation; + #end var objectAnimation: ObjectAnimation; var isArmature: Bool; var boneLayer: Null; @@ -268,12 +285,14 @@ class SwitchActionOperator { var tween: TAnim = null; public function new(animation: Animation) { - + #if lnx_skin if(Std.isOfType(animation, BoneAnimation)) { boneAnimation = cast animation; this.isArmature = true; } - else { + else + #end + { objectAnimation = cast animation; this.isArmature = false; } @@ -338,6 +357,7 @@ class SwitchActionOperator { } } +#if lnx_skin class SimpleBiPedalIK { var object: Object; @@ -477,6 +497,7 @@ class SimpleBiPedalIK { } } +#end @:enum abstract SelectAction(Int) from Int to Int { var action1 = 0; diff --git a/leenkx/Sources/leenkx/renderpath/FSR1.hx b/leenkx/Sources/leenkx/renderpath/FSR1.hx new file mode 100644 index 0000000..d75e4bf --- /dev/null +++ b/leenkx/Sources/leenkx/renderpath/FSR1.hx @@ -0,0 +1,56 @@ +package leenkx.renderpath; + +import iron.RenderPath; + +/** + * AMD FidelityFX Super Resolution (FSR V1) - Spatial Upscaling + * MIT License - + * Quality sharpness values: + * - Ultra_Quality: 0.0 + * - Quality: 0.25 + * - Balanced: 0.5 + * - Performance: 0.75 + */ +class FSR1 { + + static var path: RenderPath; + + public static inline var ULTRA_QUALITY: Float = 0.0; + public static inline var QUALITY: Float = 0.25; + public static inline var BALANCED: Float = 0.5; + public static inline var PERFORMANCE: Float = 0.75; + + public static function init(_path: RenderPath) { + path = _path; + + #if rp_fsr1 + path.loadShader("shader_datas/fsr1_easu_pass/fsr1_easu_pass"); + path.loadShader("shader_datas/fsr1_rcas_pass/fsr1_rcas_pass"); + #end + } + + public static function getSharpnessFromPreset(preset: String): Float { + return switch(preset) { + case "Ultra_Quality": ULTRA_QUALITY; + case "Quality": QUALITY; + case "Balanced": BALANCED; + case "Performance": PERFORMANCE; + default: QUALITY; + }; + } + + public static function applyRCAS(inputTarget: String, outputTarget: String = null) { + #if rp_fsr1 + if (path == null) return; + + if (outputTarget != null) { + path.setTarget(outputTarget); + } else { + path.setTarget(""); + } + + path.bindTarget(inputTarget, "tex"); + path.drawShader("shader_datas/fsr1_rcas_pass/fsr1_rcas_pass"); + #end + } +} diff --git a/leenkx/Sources/leenkx/renderpath/Inc.hx b/leenkx/Sources/leenkx/renderpath/Inc.hx index 77f4c69..e79fe5f 100644 --- a/leenkx/Sources/leenkx/renderpath/Inc.hx +++ b/leenkx/Sources/leenkx/renderpath/Inc.hx @@ -14,6 +14,13 @@ class Inc { static var spotIndex = 0; static var lastFrame = -1; + #if lnx_shadowmap_atlas + static var tilesToRemove: Array = []; + #if lnx_shadowmap_atlas_lod + static var tilesToChangeSize: Array = []; + #end + #end + #if ((rp_voxels != 'Off') && lnx_config) static var voxelsCreated = false; #end @@ -149,12 +156,39 @@ class Inc { } public static function bindShadowMapAtlas() { + var hasAtlas = false; + for (atlas in ShadowMapAtlas.shadowMapAtlases) { path.bindTarget(atlas.target, atlas.target); + hasAtlas = true; } + + if (!hasAtlas) { + #if lnx_shadowmap_atlas_single_map + path.bindTarget("empty_shadowmap", "shadowMapAtlas"); + #else + path.bindTarget("empty_shadowmap", "shadowMapAtlasSun"); + path.bindTarget("empty_shadowmap", "shadowMapAtlasPoint"); + path.bindTarget("empty_shadowmap", "shadowMapAtlasSpot"); + #end + } + #if rp_shadowmap_transparent + var hasAtlasT = false; + for (atlas in ShadowMapAtlas.shadowMapAtlasesTransparent) { path.bindTarget(atlas.target, atlas.target); + hasAtlasT = true; + } + + if (!hasAtlasT) { + #if lnx_shadowmap_atlas_single_map + path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasTransparent"); + #else + path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasSunTransparent"); + path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasPointTransparent"); + path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasSpotTransparent"); + #end } #end } @@ -223,9 +257,9 @@ class Inc { #end for (atlas in ShadowMapAtlas.shadowMapAtlases) { - var tilesToRemove = []; + tilesToRemove.resize(0); #if lnx_shadowmap_atlas_lod - var tilesToChangeSize = []; + tilesToChangeSize.resize(0); #end var shadowmap = getShadowMapAtlas(atlas, false); @@ -296,13 +330,14 @@ class Inc { path.currentFace = -1; } path.endStream(); + path.currentG = null; } #if rp_shadowmap_transparent for (atlas in ShadowMapAtlas.shadowMapAtlasesTransparent) { - var tilesToRemove = []; + tilesToRemove.resize(0); #if lnx_shadowmap_atlas_lod - var tilesToChangeSize = []; + tilesToChangeSize.resize(0); #end var shadowmap = getShadowMapAtlas(atlas, true); @@ -374,6 +409,8 @@ class Inc { } path.endStream(); + path.currentG = null; + #if lnx_shadowmap_atlas_lod for (tile in tilesToChangeSize) { tilesToRemove.push(tile); @@ -382,9 +419,6 @@ class Inc { if (newTile != null) atlas.activeTiles.push(newTile); } - // update point light data after changing size of tiles to avoid render issues - updatePointLightAtlasData(false); - // update point light data after changing size of tiles to avoid render issues updatePointLightAtlasData(true); #end @@ -554,8 +588,16 @@ class Inc { } if (superSample != config.rp_supersample) { superSample = config.rp_supersample; + + var inVRPresent = false; + #if (kha_webgl && lnx_vr) + inVRPresent = kha.vr.VrInterface.instance != null && + kha.vr.VrInterface.instance.IsPresenting(); + #end + for (rt in path.renderTargets) { if (rt.raw.width == 0 && rt.raw.scale != null) { + // VR present mode renderpath automatically overrides Inc.superSample to 4.0 rt.raw.scale = getSuperSampling(); } } @@ -1372,6 +1414,10 @@ class ShadowMapAtlas { return; } + mainTile.forEachTileLinked(function(tile) { + tile.isTransparent = transparent; + }); + atlas.activeTiles.push(mainTile); // notify the tile on light remove light.tileNotifyOnRemove = mainTile.notifyOnLightRemove; @@ -1560,6 +1606,7 @@ class ShadowMapTile { public var size:Int; public var tiles:Array = []; public var linkedTile:ShadowMapTile = null; + public var isTransparent:Bool = false; // track for tile #if lnx_shadowmap_atlas_lod public var parentTile: ShadowMapTile = null; @@ -1791,7 +1838,11 @@ class ShadowMapTile { public function freeTile(): Void { // prevent duplicates if (light != null && unlockLight) { - light.lightInAtlas = false; + if (isTransparent) { + light.lightInAtlasTransparent = false; + } else { + light.lightInAtlas = false; + } unlockLight = false; } diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx index 9a415ef..4024397 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx @@ -15,9 +15,9 @@ class RenderPathDeferred { static var bloomUpsampler: Upsampler; #end - #if (rp_ssgi == "SSGI") - static var ssgitex = "singleb"; - static var ssgitexb = "singleb"; + #if rp_ssgi + static var ssgitex = "ssgi_a"; + static var ssgitexb = "ssgi_b"; #end public static inline function setTargetMeshes() { @@ -187,40 +187,32 @@ class RenderPathDeferred { path.loadShader("shader_datas/copy_pass/copy_pass"); #end - #if ((rp_ssgi == "RTGI") || (rp_ssgi == "RTAO")) - { - path.loadShader("shader_datas/ssgi_pass/ssgi_pass"); - path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); - path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); - } - #elseif (rp_ssgi == "SSAO") + #if rp_ssao { path.loadShader("shader_datas/ssao_pass/ssao_pass"); path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); } - #elseif (rp_ssgi == "SSGI") + #end + + #if rp_ssgi { path.loadShader("shader_datas/ssgi_pass/ssgi_pass"); - path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); - path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); + path.loadShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_x"); + path.loadShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_y"); } #end - #if (rp_ssgi != "Off") + #if rp_ssao { var t = new RenderTargetRaw(); t.name = "singlea"; t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - #if (rp_ssgi == "SSGI") - t.format = "RGBA32"; - #else t.format = "R8"; - #end t.scale = Inc.getSuperSampling(); - #if rp_ssgi_half + #if rp_ssao_half t.scale *= 0.5; #end path.createRenderTarget(t); @@ -230,11 +222,35 @@ class RenderPathDeferred { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - #if (rp_ssgi == "SSGI") + t.format = "R8"; + t.scale = Inc.getSuperSampling(); + #if rp_ssao_half + t.scale *= 0.5; + #end + path.createRenderTarget(t); + } + #end + + #if rp_ssgi + { + var t = new RenderTargetRaw(); + t.name = "ssgi_a"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); t.format = "RGBA32"; - #else - t.format = "R8"; + t.scale = Inc.getSuperSampling(); + #if rp_ssgi_half + t.scale *= 0.5; #end + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "ssgi_b"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; t.scale = Inc.getSuperSampling(); #if rp_ssgi_half t.scale *= 0.5; @@ -252,9 +268,6 @@ class RenderPathDeferred { t.displayp = Inc.getDisplayp(); t.format = "R8"; t.scale = Inc.getSuperSampling(); - #if rp_ssgi_half // Do we keep this ? - t.scale *= 0.5; - #end path.createRenderTarget(t); var t = new RenderTargetRaw(); @@ -264,37 +277,6 @@ class RenderPathDeferred { t.displayp = Inc.getDisplayp(); t.format = "R8"; t.scale = Inc.getSuperSampling(); - #if rp_ssgi_half - t.scale *= 0.5; - #end - path.createRenderTarget(t); - } - #end - - #if rp_volumetriclight - { - var t = new RenderTargetRaw(); - t.name = "volumetrica"; - t.width = 0; - t.height = 0; - t.displayp = Inc.getDisplayp(); - t.format = "R8"; - t.scale = Inc.getSuperSampling(); - #if rp_ssgi_half // Do we keep this ? - t.scale *= 0.5; - #end - path.createRenderTarget(t); - - var t = new RenderTargetRaw(); - t.name = "volumetricb"; - t.width = 0; - t.height = 0; - t.displayp = Inc.getDisplayp(); - t.format = "R8"; - t.scale = Inc.getSuperSampling(); - #if rp_ssgi_half - t.scale *= 0.5; - #end path.createRenderTarget(t); } #end @@ -381,6 +363,14 @@ class RenderPathDeferred { } #end + #if rp_fsr1 + { + path.loadShader("shader_datas/fsr1_easu_pass/fsr1_easu_pass"); + path.loadShader("shader_datas/fsr1_rcas_pass/fsr1_rcas_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + #if rp_autoexposure { var t = new RenderTargetRaw(); @@ -401,7 +391,7 @@ class RenderPathDeferred { } #end - #if (rp_ssr_half || rp_ssgi_half || rp_voxels != "Off") //we need half depth for resolve voxels shaders + #if (rp_ssr_half || rp_ssao_half || rp_ssgi_half || rp_voxels != "Off") //we need half depth for resolve voxels shaders { path.loadShader("shader_datas/downsample_depth/downsample_depth"); var t = new RenderTargetRaw(); @@ -423,6 +413,7 @@ class RenderPathDeferred { t.displayp = Inc.getDisplayp(); t.format = Inc.getHdrFormat(); t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; path.createRenderTarget(t); } #end @@ -511,6 +502,31 @@ class RenderPathDeferred { } #end + // TODO: Re-investigate creating fallback empty shadowmaps for WebGL when no lights exist + #if lnx_shadowmap_atlas + { + var t = new RenderTargetRaw(); + t.name = "empty_shadowmap"; + t.width = 1; + t.height = 1; + t.format = "DEPTH16"; + path.createRenderTarget(t); + path.setTarget("empty_shadowmap"); + path.clearTarget(null, 1.0); + + #if rp_shadowmap_transparent + var t2 = new RenderTargetRaw(); + t2.name = "empty_shadowmap_transparent"; + t2.width = 1; + t2.height = 1; + t2.format = "RGBA64"; + path.createRenderTarget(t2); + path.setTarget("empty_shadowmap_transparent"); + path.clearTarget(0xffffffff, null); + #end + } + #end + #if rp_chromatic_aberration { path.loadShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); @@ -536,7 +552,7 @@ class RenderPathDeferred { #if (rp_ssrefr || lnx_voxelgi_refract) { path.setTarget("gbuffer_refraction"); - path.clearTarget(0xffffff00); + path.clearTarget(0xffff00ff); } #end @@ -585,7 +601,7 @@ class RenderPathDeferred { } #end - #if (rp_ssr_half || rp_ssgi_half || (rp_voxels != "Off")) + #if (rp_ssr_half || rp_ssao_half || rp_ssgi_half || (rp_voxels != "Off")) path.setTarget("half"); path.bindTarget("_main", "texdepth"); path.drawShader("shader_datas/downsample_depth/downsample_depth"); @@ -600,9 +616,9 @@ class RenderPathDeferred { #end #end - #if (rp_ssgi == "SSAO") + #if rp_ssao { - if (leenkx.data.Config.raw.rp_ssgi != false) { + if (leenkx.data.Config.raw.rp_ssao != false) { path.setTarget("singlea"); path.bindTarget("_main", "gbufferD"); path.bindTarget("gbuffer0", "gbuffer0"); @@ -619,10 +635,12 @@ class RenderPathDeferred { path.drawShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); } } - #elseif (rp_ssgi == "SSGI") + #end + + #if rp_ssgi { if (leenkx.data.Config.raw.rp_ssgi != false) { - path.setTarget("singlea"); + path.setTarget("ssgi_a"); path.bindTarget("_main", "gbufferD"); path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer1", "gbuffer1"); @@ -631,29 +649,20 @@ class RenderPathDeferred { path.bindTarget("gbuffer_emission", "gbufferEmission"); } #end - #if rp_gbuffer2 - path.bindTarget("gbuffer2", "sveloc"); - #end - #if rp_shadowmap - { - #if lnx_shadowmap_atlas - Inc.bindShadowMapAtlas(); - #else - Inc.bindShadowMap(); - #end - } - #end path.drawShader("shader_datas/ssgi_pass/ssgi_pass"); - path.setTarget("singleb"); - path.bindTarget("singlea", "tex"); - path.bindTarget("gbuffer0", "gbuffer0"); - path.drawShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); - path.setTarget("singlea"); - path.bindTarget("singleb", "tex"); + path.setTarget("ssgi_b"); + path.bindTarget("ssgi_a", "tex"); path.bindTarget("gbuffer0", "gbuffer0"); - path.drawShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); + path.bindTarget("_main", "gbufferD"); + path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_x"); + + path.setTarget("ssgi_a"); + path.bindTarget("ssgi_b", "tex"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("_main", "gbufferD"); + path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_y"); } } #end @@ -740,9 +749,9 @@ class RenderPathDeferred { } #end - #if (rp_ssgi != "Off") + #if rp_ssao { - if (leenkx.data.Config.raw.rp_ssgi != false) { + if (leenkx.data.Config.raw.rp_ssao != false) { path.bindTarget("singlea", "ssaotex"); } else { @@ -751,6 +760,14 @@ class RenderPathDeferred { } #end + #if rp_ssgi + { + if (leenkx.data.Config.raw.rp_ssgi != false) { + path.bindTarget("ssgi_a", "ssgitex"); + } + } + #end + var voxelao_pass = false; #if (rp_voxels != "Off") if (leenkx.data.Config.raw.rp_gi != false) @@ -848,24 +865,9 @@ class RenderPathDeferred { } #end - #if rp_volumetriclight + #if (rp_translucency && !rp_ssrefr) { - path.setTarget("volumetrica"); - path.bindTarget("_main", "gbufferD"); - #if lnx_shadowmap_atlas - Inc.bindShadowMapAtlas(); - #else - Inc.bindShadowMap(); - #end - path.drawShader("shader_datas/volumetric_light/volumetric_light"); - - path.setTarget("volumetricb"); - path.bindTarget("volumetrica", "tex"); - path.drawShader("shader_datas/blur_bilat_pass/blur_bilat_pass_x"); - - path.setTarget("tex"); - path.bindTarget("volumetricb", "tex"); - path.drawShader("shader_datas/blur_bilat_blend_pass/blur_bilat_blend_pass_y"); + Inc.drawTranslucency("tex"); } #end @@ -937,12 +939,6 @@ class RenderPathDeferred { } #end - #if (rp_translucency && !rp_ssrefr) - { - Inc.drawTranslucency("tex"); - } - #end - #if rp_ssrefr { if (leenkx.data.Config.raw.rp_ssrefr != false) @@ -982,19 +978,55 @@ class RenderPathDeferred { path.drawMeshes("refraction"); - path.setTarget("tex"); - path.bindTarget("tex", "tex"); - path.bindTarget("refr", "tex1"); + path.setTarget("buf"); + path.bindTarget("tex", "tex"); // scene with refractive objects + path.bindTarget("refr", "tex1"); // background without refractive objects path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); + + path.setTarget("tex"); + path.bindTarget("buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); } } #end + #if rp_chromatic_aberration + { + path.setTarget("buf"); + path.bindTarget("tex", "tex"); + path.drawShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); + path.setTarget("tex"); + path.bindTarget("buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if rp_volumetriclight + { + path.setTarget("volumetrica"); + path.bindTarget("_main", "gbufferD"); + #if lnx_shadowmap_atlas + Inc.bindShadowMapAtlas(); + #else + Inc.bindShadowMap(); + #end + path.drawShader("shader_datas/volumetric_light/volumetric_light"); + + path.setTarget("volumetricb"); + path.bindTarget("volumetrica", "tex"); + path.drawShader("shader_datas/blur_bilat_pass/blur_bilat_pass_x"); + + path.setTarget("tex"); + path.bindTarget("volumetricb", "tex"); + path.drawShader("shader_datas/blur_bilat_blend_pass/blur_bilat_blend_pass_y"); + } + #end + #if ((rp_motionblur == "Camera") || (rp_motionblur == "Object")) { if (leenkx.data.Config.raw.rp_motionblur != false) { @@ -1029,22 +1061,6 @@ class RenderPathDeferred { } #end - #if rp_chromatic_aberration - { - path.setTarget("buf"); - path.bindTarget("tex", "tex"); - path.drawShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); - path.setTarget("tex"); - path.bindTarget("buf", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - } - #end - - // We are just about to enter compositing, add more custom passes here - // #if rp_custom_pass - // { - // } - // #end // Begin compositor #if rp_autoexposure @@ -1084,6 +1100,7 @@ class RenderPathDeferred { } #end path.setTarget(target); + path.clearTarget(0x00000000); path.bindTarget("tex", "tex"); #if rp_compositordepth @@ -1167,12 +1184,58 @@ class RenderPathDeferred { } #end + #if rp_fsr1 + { + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + #if (rp_supersampling == 4) + var fsrSource = "buf"; + var fsrDest = "buf"; + #else + path.setTarget("bufb"); + path.bindTarget(framebuffer != "" ? framebuffer : "buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + var fsrSource = "bufb"; + var fsrDest = ""; + #end + #else + #if (rp_supersampling == 4) + var fsrSource = "buf"; + var fsrDest = "buf"; + #else + path.setTarget("bufa"); + path.bindTarget(target != "" ? target : "buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + var fsrSource = "bufa"; + var fsrDest = ""; + #end + #end + + path.setTarget(fsrDest); + path.bindTarget(fsrSource, "tex"); + path.drawShader("shader_datas/fsr1_rcas_pass/fsr1_rcas_pass"); + } + #end + #if (rp_supersampling == 4) { var finalTarget = ""; path.setTarget(finalTarget); path.bindTarget(framebuffer, "tex"); - path.drawShader("shader_datas/supersample_resolve/supersample_resolve"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #end + + // VR: Composite final output to XR framebuffer + #if (kha_webgl && lnx_vr) + if (iron.RenderPath.isVRPresenting()) { + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + var vrCompositeSource = framebuffer; + #else + var vrCompositeSource = "tex"; + #end + if (vrCompositeSource == "") vrCompositeSource = "tex"; + + path.compositeToXR(vrCompositeSource); } #end } diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx index 6b8b904..77ac39a 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx @@ -29,7 +29,12 @@ class RenderPathForward { } #else { - path.setTarget(""); + var isVR = iron.RenderPath.isVRPresenting() || iron.RenderPath.isVRSimulateMode(); + if (isVR) { + path.setTarget("lbuffer0"); + } else { + path.setTarget(""); + } } #end } @@ -251,25 +256,18 @@ class RenderPathForward { #end #end - #if (rp_volumetriclight || rp_ssgi != "Off") + #if rp_volumetriclight { - #if (rp_volumetriclight) path.loadShader("shader_datas/volumetric_light/volumetric_light"); path.loadShader("shader_datas/blur_bilat_pass/blur_bilat_pass_x"); path.loadShader("shader_datas/blur_bilat_blend_pass/blur_bilat_blend_pass_y"); - #end - var t = new RenderTargetRaw(); t.name = "singlea"; t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - #if (rp_ssgi == "SSGI") - t.format = "RGBA32"; - #else t.format = "R8"; - #end t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); @@ -278,11 +276,51 @@ class RenderPathForward { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - #if (rp_ssgi == "SSGI") - t.format = "RGBA32"; - #else t.format = "R8"; - #end + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_ssao + { + var t = new RenderTargetRaw(); + t.name = "singlea"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R8"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "singleb"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R8"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_ssgi + { + var t = new RenderTargetRaw(); + t.name = "ssgi_a"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "ssgi_b"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); } @@ -308,7 +346,15 @@ class RenderPathForward { } #end - #if (rp_ssr_half || rp_ssgi_half || (rp_voxels != "Off")) + #if rp_fsr1 + { + path.loadShader("shader_datas/fsr1_easu_pass/fsr1_easu_pass"); + path.loadShader("shader_datas/fsr1_rcas_pass/fsr1_rcas_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if (rp_ssr_half || rp_ssao_half || rp_ssgi_half || (rp_voxels != "Off")) { path.loadShader("shader_datas/downsample_depth/downsample_depth"); var t = new RenderTargetRaw(); @@ -422,14 +468,21 @@ class RenderPathForward { #if (rp_ssrefr || lnx_voxelgi_refract) { - path.setTarget("gbuffer_refraction"); // Only clear gbuffer0 - path.clearTarget(0xffffff00); + path.setTarget("gbuffer_refraction"); + path.clearTarget(0xffff00ff); } #end #if rp_depthprepass { - path.drawMeshes("depth"); + #if rp_stereo + var isVR = iron.RenderPath.isVRPresenting() || iron.RenderPath.isVRSimulateMode(); + if (!isVR) { + #end + path.drawMeshes("depth"); + #if rp_stereo + } + #end } #end @@ -461,16 +514,23 @@ class RenderPathForward { #if rp_stereo { path.drawStereo(drawMeshes); + if (iron.RenderPath.isVRPresenting()) { + #if (kha_webgl && lnx_vr) + // split-screen lbuffer0 to XR framebuffer + path.compositeToXR("lbuffer0"); + #end + return; + } } #else { - RenderPathCreator.drawMeshes(); + drawMeshes(); } #end #if (rp_render_to_texture || rp_voxels != "Off") { - #if (rp_ssr_half || rp_ssgi_half || rp_voxels != "Off") + #if (rp_ssr_half || rp_ssao_half || rp_ssgi_half || rp_voxels != "Off") path.setTarget("half"); path.bindTarget("_main", "texdepth"); path.drawShader("shader_datas/downsample_depth/downsample_depth"); @@ -515,8 +575,7 @@ class RenderPathForward { path.drawMeshes("refraction"); - path.setTarget("lbuffer0"); - + path.setTarget("bufa"); path.bindTarget("lbuffer0", "tex"); path.bindTarget("refr", "tex1"); path.bindTarget("_main", "gbufferD"); @@ -525,6 +584,9 @@ class RenderPathForward { path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); + path.setTarget("lbuffer0"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); } } #end @@ -595,7 +657,7 @@ class RenderPathForward { path.drawMeshes("refraction"); - path.setTarget("lbuffer0"); + path.setTarget("bufa"); path.bindTarget("lbuffer0", "tex"); path.bindTarget("refr", "tex1"); path.bindTarget("_main", "gbufferD"); @@ -604,6 +666,9 @@ class RenderPathForward { path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); + path.setTarget("lbuffer0"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); } } #end @@ -614,6 +679,18 @@ class RenderPathForward { } #end + #if rp_chromatic_aberration + { + path.setTarget("bufa"); + path.bindTarget("lbuffer0", "tex"); + path.drawShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); + + path.setTarget("lbuffer0"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #end + #if rp_volumetriclight { path.setTarget("singlea"); @@ -654,18 +731,6 @@ class RenderPathForward { } #end - #if rp_chromatic_aberration - { - path.setTarget("bufa"); - path.bindTarget("lbuffer0", "tex"); - path.drawShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); - - path.setTarget("lbuffer0"); - path.bindTarget("bufa", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - } - #end - #if (rp_supersampling == 4) var framebuffer = "buf"; #else @@ -685,6 +750,7 @@ class RenderPathForward { } #end path.setTarget(target); + path.clearTarget(0x00000000); #if rp_compositordepth { @@ -732,6 +798,40 @@ class RenderPathForward { } #end + #if rp_fsr1 + { + // FSR1 RCAS sharpening pass applied after AA, expects sRGB [0-1] input + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + #if (rp_supersampling == 4) + var fsrSource = "buf"; + var fsrDest = "buf"; + #else + // SMAA outputs to framebuffer which needs an intermediate buffer + path.setTarget("bufb"); + path.bindTarget(framebuffer != "" ? framebuffer : "buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + var fsrSource = "bufb"; + var fsrDest = ""; + #end + #else + #if (rp_supersampling == 4) + var fsrSource = "buf"; + var fsrDest = "buf"; + #else + path.setTarget("bufa"); + path.bindTarget(target != "" ? target : "buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + var fsrSource = "bufa"; + var fsrDest = ""; + #end + #end + + path.setTarget(fsrDest); + path.bindTarget(fsrSource, "tex"); + path.drawShader("shader_datas/fsr1_rcas_pass/fsr1_rcas_pass"); + } + #end + #if (rp_supersampling == 4) { var finalTarget = ""; diff --git a/leenkx/Sources/leenkx/system/Starter.hx b/leenkx/Sources/leenkx/system/Starter.hx index e8a8ca2..c6d94a5 100644 --- a/leenkx/Sources/leenkx/system/Starter.hx +++ b/leenkx/Sources/leenkx/system/Starter.hx @@ -96,6 +96,25 @@ class Starter { } #end + #if (js && lnx_jolt) + function loadLibJolt(name: String) { + kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { + js.Syntax.code("(1,eval)({0})", b.toString()); + #if kha_krom + js.Syntax.code("Jolt({print:function(s){iron.log(s);},instantiateWasm:function(imports,successCallback) { + var wasmbin = Krom.loadBlob('jolt.wasm.wasm'); + var module = new WebAssembly.Module(wasmbin); + var inst = new WebAssembly.Instance(module,imports); + successCallback(inst); + return inst.exports; + }}).then(function(m){ Jolt=m; tasks--; start();})"); + #else + js.Syntax.code("Jolt({print:function(s){iron.log(s);},locateFile:function(f){return 'jolt.wasm.wasm';}}).then(function(m){ Jolt=m; tasks--; start();})"); + #end + }); + } + #end + #if (js && lnx_navigation) function loadLib(name: String) { kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { @@ -126,6 +145,11 @@ class Starter { #end #end + #if (js && lnx_jolt) + tasks++; + loadLibJolt("jolt.wasm.js"); + #end + #if (js && lnx_navigation) tasks++; #if kha_krom diff --git a/leenkx/Sources/leenkx/trait/physics/KinematicCharacterController.hx b/leenkx/Sources/leenkx/trait/physics/KinematicCharacterController.hx index ad6d2df..2bc8dd3 100644 --- a/leenkx/Sources/leenkx/trait/physics/KinematicCharacterController.hx +++ b/leenkx/Sources/leenkx/trait/physics/KinematicCharacterController.hx @@ -7,13 +7,11 @@ class KinematicCharacterController extends iron.Trait { public function new() { #else #if lnx_bullet - typedef KinematicCharacterController = leenkx.trait.physics.bullet.KinematicCharacterController; - + #elseif lnx_jolt + typedef KinematicCharacterController = leenkx.trait.physics.jolt.KinematicCharacterController; #else - typedef KinematicCharacterController = leenkx.trait.physics.oimo.KinematicCharacterController; - #end #end diff --git a/leenkx/Sources/leenkx/trait/physics/PhysicsCache.hx b/leenkx/Sources/leenkx/trait/physics/PhysicsCache.hx index 1d36104..b6435cf 100644 --- a/leenkx/Sources/leenkx/trait/physics/PhysicsCache.hx +++ b/leenkx/Sources/leenkx/trait/physics/PhysicsCache.hx @@ -21,6 +21,8 @@ class PhysicsCache { #if lnx_bullet var rb = object.getTrait(leenkx.trait.physics.bullet.RigidBody); + #elseif lnx_jolt + var rb = object.getTrait(leenkx.trait.physics.jolt.RigidBody); #else var rb = object.getTrait(leenkx.trait.physics.oimo.RigidBody); #end @@ -42,6 +44,9 @@ class PhysicsCache { #if lnx_bullet if (leenkx.trait.physics.bullet.PhysicsWorld.active == null) return null; return leenkx.trait.physics.bullet.PhysicsWorld.active.getContacts(rb); + #elseif lnx_jolt + if (leenkx.trait.physics.jolt.PhysicsWorld.active == null) return null; + return leenkx.trait.physics.jolt.PhysicsWorld.active.getContacts(rb); #else if (leenkx.trait.physics.oimo.PhysicsWorld.active == null) return null; return leenkx.trait.physics.oimo.PhysicsWorld.active.getContacts(rb); @@ -61,6 +66,9 @@ class PhysicsCache { #if lnx_bullet if (leenkx.trait.physics.bullet.PhysicsWorld.active == null) return null; var contacts = leenkx.trait.physics.bullet.PhysicsWorld.active.getContacts(rb); + #elseif lnx_jolt + if (leenkx.trait.physics.jolt.PhysicsWorld.active == null) return null; + var contacts = leenkx.trait.physics.jolt.PhysicsWorld.active.getContacts(rb); #else if (leenkx.trait.physics.oimo.PhysicsWorld.active == null) return null; var contacts = leenkx.trait.physics.oimo.PhysicsWorld.active.getContacts(rb); diff --git a/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx b/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx index eeb5358..eefdb48 100644 --- a/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx +++ b/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx @@ -10,6 +10,9 @@ class PhysicsConstraint extends iron.Trait { public function new() { super(); } #if lnx_bullet typedef PhysicsConstraint = leenkx.trait.physics.bullet.PhysicsConstraint; typedef ConstraintAxis = leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintAxis; + #elseif lnx_jolt + typedef PhysicsConstraint = leenkx.trait.physics.jolt.PhysicsConstraint; + typedef ConstraintAxis = leenkx.trait.physics.jolt.PhysicsConstraint.ConstraintType; #else typedef PhysicsConstraint = leenkx.trait.physics.oimo.PhysicsConstraint; typedef ConstraintAxis = leenkx.trait.physics.oimo.PhysicsConstraint.ConstraintAxis; diff --git a/leenkx/Sources/leenkx/trait/physics/PhysicsHook.hx b/leenkx/Sources/leenkx/trait/physics/PhysicsHook.hx index bd68a65..e7eb823 100644 --- a/leenkx/Sources/leenkx/trait/physics/PhysicsHook.hx +++ b/leenkx/Sources/leenkx/trait/physics/PhysicsHook.hx @@ -7,13 +7,11 @@ class PhysicsHook extends iron.Trait { public function new() { super(); } } #else #if lnx_bullet - typedef PhysicsHook = leenkx.trait.physics.bullet.PhysicsHook; - + #elseif lnx_jolt + typedef PhysicsHook = leenkx.trait.physics.jolt.PhysicsHook; #else - typedef PhysicsHook = leenkx.trait.physics.oimo.PhysicsHook; - #end #end diff --git a/leenkx/Sources/leenkx/trait/physics/PhysicsWorld.hx b/leenkx/Sources/leenkx/trait/physics/PhysicsWorld.hx index 84d386e..20cab22 100644 --- a/leenkx/Sources/leenkx/trait/physics/PhysicsWorld.hx +++ b/leenkx/Sources/leenkx/trait/physics/PhysicsWorld.hx @@ -10,6 +10,9 @@ class PhysicsWorld extends iron.Trait { public function new() { super(); } } #if lnx_bullet typedef PhysicsWorld = leenkx.trait.physics.bullet.PhysicsWorld; typedef Hit = leenkx.trait.physics.bullet.PhysicsWorld.Hit; + #elseif lnx_jolt + typedef PhysicsWorld = leenkx.trait.physics.jolt.PhysicsWorld; + typedef Hit = leenkx.trait.physics.jolt.PhysicsWorld.Hit; #else typedef PhysicsWorld = leenkx.trait.physics.oimo.PhysicsWorld; typedef Hit = leenkx.trait.physics.oimo.PhysicsWorld.Hit; diff --git a/leenkx/Sources/leenkx/trait/physics/RigidBody.hx b/leenkx/Sources/leenkx/trait/physics/RigidBody.hx index 2742418..fe0acee 100644 --- a/leenkx/Sources/leenkx/trait/physics/RigidBody.hx +++ b/leenkx/Sources/leenkx/trait/physics/RigidBody.hx @@ -8,15 +8,14 @@ class RigidBody extends iron.Trait { public function new() { super(); } } #else #if lnx_bullet - typedef RigidBody = leenkx.trait.physics.bullet.RigidBody; typedef Shape = leenkx.trait.physics.bullet.RigidBody.Shape; - + #elseif lnx_jolt + typedef RigidBody = leenkx.trait.physics.jolt.RigidBody; + typedef Shape = leenkx.trait.physics.jolt.RigidBody.Shape; #else - typedef RigidBody = leenkx.trait.physics.oimo.RigidBody; typedef Shape = leenkx.trait.physics.oimo.RigidBody.Shape; - #end #end diff --git a/leenkx/Sources/leenkx/trait/physics/SoftBody.hx b/leenkx/Sources/leenkx/trait/physics/SoftBody.hx index 818e544..57e9818 100644 --- a/leenkx/Sources/leenkx/trait/physics/SoftBody.hx +++ b/leenkx/Sources/leenkx/trait/physics/SoftBody.hx @@ -1,5 +1,7 @@ package leenkx.trait.physics; +import iron.Trait; + #if (!lnx_physics_soft) class SoftBody extends Trait { public function new() { super(); } } @@ -7,13 +9,11 @@ class SoftBody extends Trait { public function new() { super(); } } #else #if lnx_bullet - typedef SoftBody = leenkx.trait.physics.bullet.SoftBody; - + #elseif lnx_jolt + typedef SoftBody = leenkx.trait.physics.jolt.SoftBody; #else - typedef SoftBody = leenkx.trait.physics.oimo.SoftBody; - #end #end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/DebugDrawHelper.hx b/leenkx/Sources/leenkx/trait/physics/jolt/DebugDrawHelper.hx new file mode 100644 index 0000000..d2774ce --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/DebugDrawHelper.hx @@ -0,0 +1,376 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import kha.FastFloat; +import kha.System; +import iron.math.Vec4; + +#if lnx_ui +import leenkx.ui.Canvas; +#end + +using StringTools; + +enum abstract DebugDrawMode(Int) from Int to Int { + var NoDebug = 0; + var DrawWireframe = 1; + var DrawAabb = 2; + var DrawContactPoints = 4; + var DrawConstraints = 8; + var DrawConstraintLimits = 16; + var DrawRayCast = 32; + var DrawAll = 63; + + @:op(A | B) static function or(lhs:DebugDrawMode, rhs:DebugDrawMode):DebugDrawMode; + @:op(A & B) static function and(lhs:DebugDrawMode, rhs:DebugDrawMode):DebugDrawMode; +} + +class DebugDrawHelper { + static inline var contactPointSizePx = 4; + static inline var contactPointNormalColor = 0xffffffff; + + final rayCastColor:Vec4 = new Vec4(0.0, 1.0, 0.0); + final rayCastHitColor:Vec4 = new Vec4(1.0, 0.0, 0.0); + final rayCastHitPointColor:Vec4 = new Vec4(1.0, 1.0, 0.0); + final wireframeColor:Vec4 = new Vec4(0.0, 1.0, 0.0); + final aabbColor:Vec4 = new Vec4(1.0, 1.0, 0.0); + final constraintColor:Vec4 = new Vec4(0.0, 0.5, 1.0); + + final physicsWorld:PhysicsWorld; + final lines:Array = []; + final texts:Array = []; + var font:kha.Font = null; + + var rayCasts:Array = []; + var debugDrawMode:DebugDrawMode = NoDebug; + + public function new(physicsWorld:PhysicsWorld, debugDrawMode:DebugDrawMode) { + this.physicsWorld = physicsWorld; + this.debugDrawMode = debugDrawMode; + + #if lnx_ui + iron.data.Data.getFont(Canvas.defaultFontName, function(defaultFont:kha.Font) { + font = defaultFont; + }); + #end + + iron.App.notifyOnRender2D(onRender); + if (debugDrawMode & DrawRayCast != 0) { + iron.App.notifyOnFixedUpdate(function() { + rayCasts.resize(0); + }); + } + } + + public function drawLine(fromX:Float, fromY:Float, fromZ:Float, toX:Float, toY:Float, toZ:Float, r:Float, g:Float, b:Float) { + final fromScreenSpace = worldToScreenFast(new Vec4(fromX, fromY, fromZ, 1.0)); + final toScreenSpace = worldToScreenFast(new Vec4(toX, toY, toZ, 1.0)); + + if (fromScreenSpace.w == 1 || toScreenSpace.w == 1) { + lines.push({ + fromX: fromScreenSpace.x, + fromY: fromScreenSpace.y, + toX: toScreenSpace.x, + toY: toScreenSpace.y, + color: kha.Color.fromFloats(r, g, b, 1.0) + }); + } + } + + public function drawLineVec(from:Vec4, to:Vec4, color:Vec4) { + drawLine(from.x, from.y, from.z, to.x, to.y, to.z, color.x, color.y, color.z); + } + + public function drawContactPoint(pointX:Float, pointY:Float, pointZ:Float, normalX:Float, normalY:Float, normalZ:Float, distance:Float, r:Float, g:Float, b:Float) { + final contactPointScreenSpace = worldToScreenFast(new Vec4(pointX, pointY, pointZ, 1.0)); + final toScreenSpace = worldToScreenFast(new Vec4(pointX + normalX * distance, pointY + normalY * distance, pointZ + normalZ * distance, 1.0)); + + if (contactPointScreenSpace.w == 1) { + final color = kha.Color.fromFloats(r, g, b, 1.0); + + lines.push({ + fromX: contactPointScreenSpace.x - contactPointSizePx, + fromY: contactPointScreenSpace.y - contactPointSizePx, + toX: contactPointScreenSpace.x + contactPointSizePx, + toY: contactPointScreenSpace.y + contactPointSizePx, + color: color + }); + + lines.push({ + fromX: contactPointScreenSpace.x - contactPointSizePx, + fromY: contactPointScreenSpace.y + contactPointSizePx, + toX: contactPointScreenSpace.x + contactPointSizePx, + toY: contactPointScreenSpace.y - contactPointSizePx, + color: color + }); + + if (toScreenSpace.w == 1) { + lines.push({ + fromX: contactPointScreenSpace.x, + fromY: contactPointScreenSpace.y, + toX: toScreenSpace.x, + toY: toScreenSpace.y, + color: contactPointNormalColor + }); + } + } + } + + public function rayCast(rayCastData:TRayCastData) { + rayCasts.push(rayCastData); + } + + function drawRayCast(f:Vec4, t:Vec4, hit:Bool) { + final from = worldToScreenFast(f.clone()); + final to = worldToScreenFast(t.clone()); + var c:kha.Color; + + if (from.w == 1 && to.w == 1) { + if (hit) + c = kha.Color.fromFloats(rayCastHitColor.x, rayCastHitColor.y, rayCastHitColor.z); + else + c = kha.Color.fromFloats(rayCastColor.x, rayCastColor.y, rayCastColor.z); + + lines.push({ + fromX: from.x, + fromY: from.y, + toX: to.x, + toY: to.y, + color: c + }); + } + } + + function drawHitPoint(hp:Vec4) { + final hitPoint = worldToScreenFast(hp.clone()); + final c = kha.Color.fromFloats(rayCastHitPointColor.x, rayCastHitPointColor.y, rayCastHitPointColor.z); + + if (hitPoint.w == 1) { + lines.push({ + fromX: hitPoint.x - contactPointSizePx, + fromY: hitPoint.y - contactPointSizePx, + toX: hitPoint.x + contactPointSizePx, + toY: hitPoint.y + contactPointSizePx, + color: c + }); + + lines.push({ + fromX: hitPoint.x - contactPointSizePx, + fromY: hitPoint.y + contactPointSizePx, + toX: hitPoint.x + contactPointSizePx, + toY: hitPoint.y - contactPointSizePx, + color: c + }); + + if (font != null) { + texts.push({ + x: hitPoint.x, + y: hitPoint.y, + color: c, + text: 'RAYCAST HIT' + }); + } + } + } + + public function drawBox(center:Vec4, halfExtents:Vec4, color:Vec4) { + var c = center; + var h = halfExtents; + + // Bottom face + drawLine(c.x - h.x, c.y - h.y, c.z - h.z, c.x + h.x, c.y - h.y, c.z - h.z, color.x, color.y, color.z); + drawLine(c.x + h.x, c.y - h.y, c.z - h.z, c.x + h.x, c.y + h.y, c.z - h.z, color.x, color.y, color.z); + drawLine(c.x + h.x, c.y + h.y, c.z - h.z, c.x - h.x, c.y + h.y, c.z - h.z, color.x, color.y, color.z); + drawLine(c.x - h.x, c.y + h.y, c.z - h.z, c.x - h.x, c.y - h.y, c.z - h.z, color.x, color.y, color.z); + + // Top face + drawLine(c.x - h.x, c.y - h.y, c.z + h.z, c.x + h.x, c.y - h.y, c.z + h.z, color.x, color.y, color.z); + drawLine(c.x + h.x, c.y - h.y, c.z + h.z, c.x + h.x, c.y + h.y, c.z + h.z, color.x, color.y, color.z); + drawLine(c.x + h.x, c.y + h.y, c.z + h.z, c.x - h.x, c.y + h.y, c.z + h.z, color.x, color.y, color.z); + drawLine(c.x - h.x, c.y + h.y, c.z + h.z, c.x - h.x, c.y - h.y, c.z + h.z, color.x, color.y, color.z); + + // Vertical edges + drawLine(c.x - h.x, c.y - h.y, c.z - h.z, c.x - h.x, c.y - h.y, c.z + h.z, color.x, color.y, color.z); + drawLine(c.x + h.x, c.y - h.y, c.z - h.z, c.x + h.x, c.y - h.y, c.z + h.z, color.x, color.y, color.z); + drawLine(c.x + h.x, c.y + h.y, c.z - h.z, c.x + h.x, c.y + h.y, c.z + h.z, color.x, color.y, color.z); + drawLine(c.x - h.x, c.y + h.y, c.z - h.z, c.x - h.x, c.y + h.y, c.z + h.z, color.x, color.y, color.z); + } + + public function drawSphere(center:Vec4, radius:Float, color:Vec4) { + final segments = 16; + final step = Math.PI * 2 / segments; + + // XY circle + for (i in 0...segments) { + var angle1 = i * step; + var angle2 = (i + 1) * step; + drawLine( + center.x + Math.cos(angle1) * radius, center.y + Math.sin(angle1) * radius, center.z, + center.x + Math.cos(angle2) * radius, center.y + Math.sin(angle2) * radius, center.z, + color.x, color.y, color.z + ); + } + + // XZ circle + for (i in 0...segments) { + var angle1 = i * step; + var angle2 = (i + 1) * step; + drawLine( + center.x + Math.cos(angle1) * radius, center.y, center.z + Math.sin(angle1) * radius, + center.x + Math.cos(angle2) * radius, center.y, center.z + Math.sin(angle2) * radius, + color.x, color.y, color.z + ); + } + + // YZ circle + for (i in 0...segments) { + var angle1 = i * step; + var angle2 = (i + 1) * step; + drawLine( + center.x, center.y + Math.cos(angle1) * radius, center.z + Math.sin(angle1) * radius, + center.x, center.y + Math.cos(angle2) * radius, center.z + Math.sin(angle2) * radius, + color.x, color.y, color.z + ); + } + } + + public function setDebugMode(debugDrawMode:DebugDrawMode) { + this.debugDrawMode = debugDrawMode; + } + + public function getDebugMode():DebugDrawMode { + return debugDrawMode; + } + + function drawBodyWireframe(body:RigidBody) { + if (body == null || body.object == null) + return; + + var transform = body.object.transform; + var pos = transform.world.getLoc(); + var dim = transform.dim; + var halfExtents = new Vec4(dim.x * 0.5, dim.y * 0.5, dim.z * 0.5); + + drawBox(pos, halfExtents, wireframeColor); + } + + function drawBodyAabb(body:RigidBody) { + if (body == null || body.object == null) + return; + + var transform = body.object.transform; + var pos = transform.world.getLoc(); + var dim = transform.dim; + var halfExtents = new Vec4(dim.x * 0.5, dim.y * 0.5, dim.z * 0.5); + + drawBox(pos, halfExtents, aabbColor); + } + + function drawConstraintDebug(constraint:PhysicsConstraint) { + if (constraint == null || constraint.body1 == null || constraint.body2 == null) + return; + + var pos1 = constraint.body1.object.transform.world.getLoc(); + var pos2 = constraint.body2.object.transform.world.getLoc(); + + drawLineVec(pos1, pos2, constraintColor); + } + + function onRender(g:kha.graphics2.Graphics) { + if (getDebugMode() == NoDebug) { + return; + } + + // Draw physics debug info + if (debugDrawMode & DrawWireframe != 0 || debugDrawMode & DrawAabb != 0) { + for (body in physicsWorld.rbMap) { + if (debugDrawMode & DrawWireframe != 0) { + drawBodyWireframe(body); + } + if (debugDrawMode & DrawAabb != 0) { + drawBodyAabb(body); + } + } + } + + if (debugDrawMode & DrawConstraints != 0) { + for (constraint in physicsWorld.constraints) { + drawConstraintDebug(constraint); + } + } + + g.opacity = 1.0; + + for (line in lines) { + g.color = line.color; + g.drawLine(line.fromX, line.fromY, line.toX, line.toY, 1.0); + } + lines.resize(0); + + if (font != null) { + g.font = font; + g.fontSize = 12; + for (text in texts) { + g.color = text.color; + g.drawString(text.text, text.x, text.y); + } + texts.resize(0); + } + + if (debugDrawMode & DrawRayCast != 0) { + for (rayCastData in rayCasts) { + if (rayCastData.hasHit) { + drawRayCast(rayCastData.from, rayCastData.hitPoint, true); + drawHitPoint(rayCastData.hitPoint); + } else { + drawRayCast(rayCastData.from, rayCastData.to, false); + } + } + } + } + + inline function worldToScreenFast(loc:Vec4):Vec4 { + final cam = iron.Scene.active.camera; + loc.w = 1.0; + loc.applyproj(cam.VP); + + if (loc.z < -1 || loc.z > 1) { + loc.w = 0.0; + } else { + loc.x = (loc.x + 1) * 0.5 * System.windowWidth(); + loc.y = (1 - loc.y) * 0.5 * System.windowHeight(); + loc.w = 1.0; + } + + return loc; + } +} + +@:structInit +class LineData { + public var fromX:FastFloat; + public var fromY:FastFloat; + public var toX:FastFloat; + public var toY:FastFloat; + public var color:kha.Color; +} + +@:structInit +class TextData { + public var x:FastFloat; + public var y:FastFloat; + public var color:kha.Color; + public var text:String; +} + +@:structInit +typedef TRayCastData = { + var from:Vec4; + var to:Vec4; + var hasHit:Bool; + @:optional var hitPoint:Vec4; + @:optional var hitNormal:Vec4; +} + +#end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/KinematicCharacterController.hx b/leenkx/Sources/leenkx/trait/physics/jolt/KinematicCharacterController.hx new file mode 100644 index 0000000..17f9c18 --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/KinematicCharacterController.hx @@ -0,0 +1,380 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import iron.Trait; +import iron.math.Vec4; +import iron.math.Quat; +import iron.object.Transform; +import iron.object.MeshObject; + +class KinematicCharacterController extends Trait { + + var shape:ControllerShape; + public var physics:PhysicsWorld; + public var transform:Transform = null; + public var mass:Float; + public var friction:Float; + public var restitution:Float; + public var collisionMargin:Float; + public var animated:Bool; + public var group = 1; + var bodyScaleX:Float; + var bodyScaleY:Float; + var bodyScaleZ:Float; + var currentScaleX:Float; + var currentScaleY:Float; + var currentScaleZ:Float; + var jumpSpeed:Float; + + public var body:jolt.Jt.Body; + public var bodyId:jolt.Jt.BodyID; + public var ready = false; + static var nextId = 0; + public var id = 0; + public var onReady:Void->Void = null; + + static var nullvec = true; + static var vec1:jolt.Jt.Vec3; + static var quat1:jolt.Jt.Quat; + static var quat = new Quat(); + + var walkDirection:Vec4 = new Vec4(); + var gravityEnabled = true; + var gravityFactor = 1.0; + + public function new(mass = 1.0, shape = ControllerShape.Capsule, jumpSpeed = 8.0, friction = 0.5, restitution = 0.0, + collisionMargin = 0.0, animated = false, group = 1) { + super(); + + this.mass = mass; + this.jumpSpeed = jumpSpeed; + this.shape = shape; + this.friction = friction; + this.restitution = restitution; + this.collisionMargin = collisionMargin; + this.animated = animated; + this.group = group; + + notifyOnAdd(init); + notifyOnLateUpdate(lateUpdate); + notifyOnRemove(removeFromWorld); + } + + inline function withMargin(f:Float):Float { + return f + f * collisionMargin; + } + + public function notifyOnReady(f:Void->Void) { + onReady = f; + if (ready) + onReady(); + } + + public function init() { + if (ready) + return; + + transform = object.transform; + physics = PhysicsWorld.active; + + if (physics == null) { + new PhysicsWorld(); + physics = PhysicsWorld.active; + } + + #if js + // Check if Jolt is initialized - defer if not + if (!physics.physicsReady) { + haxe.Timer.delay(init, 16); + return; + } + #end + + ready = true; + + if (nullvec) { + nullvec = false; + vec1 = new jolt.Jt.Vec3(0, 0, 0); + quat1 = new jolt.Jt.Quat(0, 0, 0, 1); + } + + var joltShape:jolt.Jt.Shape = createShape(); + + var pos = transform.world.getLoc(); + var rot = new iron.math.Quat(); + rot.fromMat(transform.world); + + // Jolt uses RVec3 for world positions + var jPos = new jolt.Jt.RVec3(pos.x, pos.y, pos.z); + var jRot = new jolt.Jt.Quat(rot.x, rot.y, rot.z, rot.w); + var settings = new jolt.Jt.BodyCreationSettings(joltShape, jPos, jRot, 1, 1); + + // Use kinematic body for character controller + settings.mFriction = friction; + settings.mRestitution = restitution; + + body = physics.bodyInterface.CreateBody(settings); + bodyId = body.GetID(); + + #if hl + settings.delete(); + jPos.delete(); + jRot.delete(); + #end + + physics.bodyInterface.AddBody(bodyId, 1); + + bodyScaleX = currentScaleX = transform.scale.x; + bodyScaleY = currentScaleY = transform.scale.y; + bodyScaleZ = currentScaleZ = transform.scale.z; + + id = nextId; + nextId++; + + if (onReady != null) + onReady(); + } + + function createShape():jolt.Jt.Shape { + var t = transform; + if (shape == ControllerShape.Box) { + var halfExtent = new jolt.Jt.Vec3(withMargin(t.dim.x / 2), withMargin(t.dim.y / 2), withMargin(t.dim.z / 2)); + return new jolt.Jt.BoxShape(halfExtent); + } else if (shape == ControllerShape.Sphere) { + var width = Math.max(t.dim.x, Math.max(t.dim.y, t.dim.z)); + return new jolt.Jt.SphereShape(withMargin(width / 2)); + } else if (shape == ControllerShape.Cylinder) { + var radius = Math.max(t.dim.x, t.dim.y) / 2; + var halfHeight = t.dim.z / 2; + return new jolt.Jt.CylinderShape(withMargin(halfHeight), withMargin(radius)); + } else if (shape == ControllerShape.Capsule) { + var r = t.dim.x / 2; + var halfHeight = (t.dim.z - r * 2) / 2; + if (halfHeight < 0.01) halfHeight = 0.01; + return new jolt.Jt.CapsuleShape(withMargin(halfHeight), withMargin(r)); + } else if (shape == ControllerShape.Cone) { + var radius = Math.max(t.dim.x, t.dim.y) / 2; + var halfHeight = t.dim.z / 2; + return new jolt.Jt.CylinderShape(withMargin(halfHeight), withMargin(radius)); + } + // Default capsule + var r = t.dim.x / 2; + var halfHeight = (t.dim.z - r * 2) / 2; + if (halfHeight < 0.01) halfHeight = 0.01; + return new jolt.Jt.CapsuleShape(withMargin(halfHeight), withMargin(r)); + } + + function lateUpdate() { + if (!ready) + return; + if (object.animation != null || animated) { + syncTransform(); + } else { + var p = physics.bodyInterface.GetPosition(bodyId); + var q = physics.bodyInterface.GetRotation(bodyId); + + #if js + transform.loc.set(cast p.GetX(), cast p.GetY(), cast p.GetZ()); + #else + transform.loc.set(p.GetX(), p.GetY(), p.GetZ()); + #end + transform.rot.set(q.GetX(), q.GetY(), q.GetZ(), q.GetW()); + + #if hl + p.delete(); + q.delete(); + #end + + if (object.parent != null) { + var ptransform = object.parent.transform; + transform.loc.x -= ptransform.worldx(); + transform.loc.y -= ptransform.worldy(); + transform.loc.z -= ptransform.worldz(); + } + transform.buildMatrix(); + } + } + + public function canJump():Bool { + // Simple ground check - could be improved with raycast + return onGround(); + } + + public function onGround():Bool { + // Perform downward raycast to check ground + var pos = transform.world.getLoc(); + var from = new Vec4(pos.x, pos.y, pos.z); + var to = new Vec4(pos.x, pos.y, pos.z - 0.2); + var hit = physics.rayCast(from, to); + return hit != null; + } + + public function setJumpSpeed(jumpSpeed:Float) { + this.jumpSpeed = jumpSpeed; + } + + public function setFallSpeed(fallSpeed:Float) { + // Jolt handles this through gravity + } + + public function setMaxSlope(slopeRadians:Float) { + // Would need CharacterVirtual for proper slope handling + } + + public function getMaxSlope():Float { + return Math.PI / 4; // 45 degrees default + } + + public function setMaxJumpHeight(maxJumpHeight:Float) { + // Calculate jump speed from height: v = sqrt(2 * g * h) + var g = physics.getGravity().length(); + jumpSpeed = Math.sqrt(2 * g * maxJumpHeight); + } + + public function setWalkDirection(dir:Vec4) { + walkDirection.setFrom(dir); + var vel = new jolt.Jt.Vec3(dir.x, dir.y, dir.z); + physics.bodyInterface.SetLinearVelocity(bodyId, vel); + #if hl vel.delete(); #end + } + + public function setUpInterpolate(value:Bool) { + // Not directly applicable in Jolt kinematic body + } + + public function jump() { + var currentVel = physics.bodyInterface.GetLinearVelocity(bodyId); + var vel = new jolt.Jt.Vec3(currentVel.GetX(), currentVel.GetY(), jumpSpeed); + physics.bodyInterface.SetLinearVelocity(bodyId, vel); + #if hl currentVel.delete(); vel.delete(); #end + } + + public function removeFromWorld() { + if (physics != null && ready) { + physics.bodyInterface.RemoveBody(bodyId); + physics.bodyInterface.DestroyBody(bodyId); + } + } + + public function activate() { + physics.bodyInterface.ActivateBody(bodyId); + } + + public function disableGravity() { + gravityEnabled = false; + physics.bodyInterface.SetGravityFactor(bodyId, 0.0); + } + + public function enableGravity() { + gravityEnabled = true; + physics.bodyInterface.SetGravityFactor(bodyId, gravityFactor); + } + + public function setGravity(f:Float) { + gravityFactor = f / 9.81; // Normalize + if (gravityEnabled) { + physics.bodyInterface.SetGravityFactor(bodyId, gravityFactor); + } + } + + public function setActivationState(newState:Int) { + if (newState == ControllerActivationState.NoDeactivation) { + // Keep active - Jolt handles this differently + activate(); + } + } + + public function setFriction(f:Float) { + physics.bodyInterface.SetFriction(bodyId, f); + this.friction = f; + } + + public function syncTransform() { + var t = transform; + t.buildMatrix(); + var pos = t.world.getLoc(); + var rot = new iron.math.Quat(); + rot.fromMat(t.world); + + // Jolt uses RVec3 for world positions + var p = new jolt.Jt.RVec3(pos.x, pos.y, pos.z); + var q = new jolt.Jt.Quat(rot.x, rot.y, rot.z, rot.w); + physics.bodyInterface.SetPosition(bodyId, p, 0); + physics.bodyInterface.SetRotation(bodyId, q, 0); + #if hl p.delete(); q.delete(); #end + + activate(); + } + + public function getLinearVelocity():Vec4 { + var vel = physics.bodyInterface.GetLinearVelocity(bodyId); + var result = new Vec4(vel.GetX(), vel.GetY(), vel.GetZ()); + #if hl vel.delete(); #end + return result; + } + + public function setLinearVelocity(velocity:Vec4) { + var vel = new jolt.Jt.Vec3(velocity.x, velocity.y, velocity.z); + physics.bodyInterface.SetLinearVelocity(bodyId, vel); + #if hl vel.delete(); #end + } + + public function getPosition():Vec4 { + var pos = physics.bodyInterface.GetPosition(bodyId); + var result = new Vec4(pos.GetX(), pos.GetY(), pos.GetZ()); + #if hl pos.delete(); #end + return result; + } + + public function setPosition(position:Vec4) { + var p = new jolt.Jt.RVec3(position.x, position.y, position.z); + physics.bodyInterface.SetPosition(bodyId, p, 0); + #if hl p.delete(); #end + } + + public function warp(position:Vec4) { + setPosition(position); + var zeroVel = new jolt.Jt.Vec3(0, 0, 0); + physics.bodyInterface.SetLinearVelocity(bodyId, zeroVel); + #if hl zeroVel.delete(); #end + } + + public function move(direction:Vec4, speed:Float) { + var moveVel = new Vec4(direction.x * speed, direction.y * speed, direction.z * speed); + + // Preserve vertical velocity for jumping/falling + var currentVel = physics.bodyInterface.GetLinearVelocity(bodyId); + var vel = new jolt.Jt.Vec3(moveVel.x, moveVel.y, currentVel.GetZ()); + physics.bodyInterface.SetLinearVelocity(bodyId, vel); + #if hl currentVel.delete(); vel.delete(); #end + } + + public function getGroundState():Int { + if (onGround()) { + return 0; // OnGround + } + return 3; // InAir + } + + public function isSupported():Bool { + return onGround(); + } +} + +@:enum abstract ControllerShape(Int) from Int to Int { + var Box = 0; + var Sphere = 1; + var ConvexHull = 2; + var Cone = 3; + var Cylinder = 4; + var Capsule = 5; +} + +@:enum abstract ControllerActivationState(Int) from Int to Int { + var Active = 1; + var NoDeactivation = 4; + var NoSimulation = 5; +} + +#end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsConstraint.hx b/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsConstraint.hx new file mode 100644 index 0000000..b125424 --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsConstraint.hx @@ -0,0 +1,405 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import iron.Trait; +import iron.math.Vec4; +import iron.math.Quat; +import iron.math.Mat4; +import iron.object.Object; + +@:enum abstract ConstraintType(Int) from Int to Int { + var Fixed = 0; + var Point = 1; + var Hinge = 2; + var Slider = 3; + var Piston = 4; + var Generic = 5; + var GenericSpring = 6; + var Distance = 7; +} + +class PhysicsConstraint extends Trait { + public var id:Int; + public var physics:PhysicsWorld; + public var body1:RigidBody; + public var body2:RigidBody; + public var type:ConstraintType; + public var con:jolt.Jt.Constraint; + public var conReady:Bool = false; + public var disableCollisions:Bool; + + var body1Obj:Object; + var body2Obj:Object; + var limits:Array; + var breakingThreshold:Float; + + static var nextId = 0; + + public function new(body1:Object, body2:Object, type:ConstraintType, disableCollisions:Bool = false, breakingThreshold:Float = 0.0, + limits:Array = null) { + super(); + + this.type = type; + this.disableCollisions = disableCollisions; + this.breakingThreshold = breakingThreshold; + this.limits = limits; + this.id = nextId++; + this.body1Obj = body1; + this.body2Obj = body2; + + notifyOnInit(function() { + this.body1 = body1.getTrait(RigidBody); + this.body2 = body2.getTrait(RigidBody); + tryInit(); + }); + + notifyOnRemove(removeFromWorld); + } + + function tryInit() { + if (this.body1 != null && this.body1.ready && this.body2 != null && this.body2.ready) { + init(); + } else if (this.body1 != null || this.body2 != null) { + // Bodies exist but not ready yet, retry next frame + iron.App.notifyOnUpdate(retryInit); + } + } + + function retryInit() { + iron.App.removeUpdate(retryInit); + tryInit(); + } + + function init() { + physics = PhysicsWorld.active; + + // Compute constraint frames in each body's local space (exactly matches Bullet approach) + var t = object.transform; // pivot object + var t1 = body1Obj.transform; // body1 object + var t2 = body2Obj.transform; // body2 object + + // Frame In A: pivot transform in body1's local space + var frameT = t.world.clone(); + var frameInA = t1.world.clone(); + frameInA.getInverse(frameInA); + frameT.multmat(frameInA); + frameInA = frameT.clone(); + + // Frame In B: pivot transform in body2's local space + frameT = t.world.clone(); + var frameInB = t2.world.clone(); + frameInB.getInverse(frameInB); + frameT.multmat(frameInB); + frameInB = frameT.clone(); + + // Decompose frames to get local positions and orientations + var locA = new Vec4(); + var rotA = new Quat(); + var sclA = new Vec4(); + frameInA.decompose(locA, rotA, sclA); + + var locB = new Vec4(); + var rotB = new Quat(); + var sclB = new Vec4(); + frameInB.decompose(locB, rotB, sclB); + + // Extract local axes from each frame (normalized to remove scale) + var rightA = frameInA.right().normalize(); + var upA = frameInA.up().normalize(); + var rightB = frameInB.right().normalize(); + var upB = frameInB.up().normalize(); + + // Create Jolt vectors for body1 local frame + var jPt1 = new jolt.Jt.RVec3(locA.x, locA.y, locA.z); + var jAxX1 = new jolt.Jt.Vec3(rightA.x, rightA.y, rightA.z); + var jAxY1 = new jolt.Jt.Vec3(upA.x, upA.y, upA.z); + + // Create Jolt vectors for body2 local frame + var jPt2 = new jolt.Jt.RVec3(locB.x, locB.y, locB.z); + var jAxX2 = new jolt.Jt.Vec3(rightB.x, rightB.y, rightB.z); + var jAxY2 = new jolt.Jt.Vec3(upB.x, upB.y, upB.z); + + switch (type) { + case Fixed: + var settings = new jolt.Jt.FixedConstraintSettings(); + settings.mSpace = 0; // LocalToBodyCOM + settings.mAutoDetectPoint = false; + settings.mPoint1 = jPt1; + settings.mPoint2 = jPt2; + settings.mAxisX1 = jAxX1; + settings.mAxisY1 = jAxY1; + settings.mAxisX2 = jAxX2; + settings.mAxisY2 = jAxY2; + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + case Point: + var settings = new jolt.Jt.PointConstraintSettings(); + settings.mSpace = 0; + settings.mPoint1 = jPt1; + settings.mPoint2 = jPt2; + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + case Hinge: + var settings = new jolt.Jt.HingeConstraintSettings(); + settings.mSpace = 0; + settings.mPoint1 = jPt1; + settings.mPoint2 = jPt2; + settings.mHingeAxis1 = jAxY1; + settings.mHingeAxis2 = jAxY2; + settings.mNormalAxis1 = jAxX1; + settings.mNormalAxis2 = jAxX2; + if (limits != null && limits.length >= 3 && limits[0] != 0) { + settings.mLimitsMin = limits[1]; + settings.mLimitsMax = limits[2]; + } + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + case Slider: + var settings = new jolt.Jt.SliderConstraintSettings(); + settings.mSpace = 0; + settings.mAutoDetectPoint = false; + settings.mPoint1 = jPt1; + settings.mPoint2 = jPt2; + settings.mSliderAxis1 = jAxX1; + settings.mSliderAxis2 = jAxX2; + settings.mNormalAxis1 = jAxY1; + settings.mNormalAxis2 = jAxY2; + if (limits != null && limits.length >= 3 && limits[0] != 0) { + settings.mLimitsMin = limits[1]; + settings.mLimitsMax = limits[2]; + } + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + case Distance: + var settings = new jolt.Jt.DistanceConstraintSettings(); + settings.mSpace = 0; + settings.mPoint1 = jPt1; + settings.mPoint2 = jPt2; + if (limits != null && limits.length >= 2) { + settings.mMinDistance = limits[0]; + settings.mMaxDistance = limits[1]; + } + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + case Piston: + var settings = new jolt.Jt.SliderConstraintSettings(); + settings.mSpace = 0; + settings.mAutoDetectPoint = false; + settings.mPoint1 = jPt1; + settings.mPoint2 = jPt2; + settings.mSliderAxis1 = jAxY1; + settings.mSliderAxis2 = jAxY2; + settings.mNormalAxis1 = jAxX1; + settings.mNormalAxis2 = jAxX2; + if (limits != null && limits.length >= 3 && limits[0] != 0) { + settings.mLimitsMin = limits[1]; + settings.mLimitsMax = limits[2]; + } + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + case Generic: + var settings = new jolt.Jt.SixDOFConstraintSettings(); + settings.mSpace = 0; + settings.mPosition1 = jPt1; + settings.mPosition2 = jPt2; + settings.mAxisX1 = jAxX1; + settings.mAxisY1 = jAxY1; + settings.mAxisX2 = jAxX2; + settings.mAxisY2 = jAxY2; + if (limits != null) { + applySixDOFLimits(settings); + } else { + for (i in 0...6) settings.MakeFreeAxis(i); + } + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + case GenericSpring: + var settings = new jolt.Jt.SixDOFConstraintSettings(); + settings.mSpace = 0; + settings.mPosition1 = jPt1; + settings.mPosition2 = jPt2; + settings.mAxisX1 = jAxX1; + settings.mAxisY1 = jAxY1; + settings.mAxisX2 = jAxX2; + settings.mAxisY2 = jAxY2; + if (limits != null) { + applySixDOFLimits(settings); + } else { + for (i in 0...6) settings.MakeFreeAxis(i); + } + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + + default: + var settings = new jolt.Jt.FixedConstraintSettings(); + settings.mSpace = 0; + settings.mAutoDetectPoint = false; + settings.mPoint1 = jPt1; + settings.mPoint2 = jPt2; + settings.mAxisX1 = jAxX1; + settings.mAxisY1 = jAxY1; + settings.mAxisX2 = jAxX2; + settings.mAxisY2 = jAxY2; + con = settings.Create(body1.body, body2.body); + #if hl settings.delete(); #end + } + + // Clean up temporary Jolt objects + #if hl + jPt1.delete(); + jPt2.delete(); + jAxX1.delete(); + jAxY1.delete(); + jAxX2.delete(); + jAxY2.delete(); + #end + + conReady = true; + physics.addPhysicsConstraint(this); + } + + function applySixDOFLimits(settings:jolt.Jt.SixDOFConstraintSettings) { + // Linear X (limits[0..2]): limits[0]=enabled, limits[1]=lower, limits[2]=upper + if (limits.length > 2 && limits[0] != 0) { + if (limits[1] > limits[2]) + settings.MakeFreeAxis(0); + else + settings.SetLimitedAxis(0, limits[1], limits[2]); + } else { + settings.MakeFreeAxis(0); + } + // Linear Y (limits[3..5]) + if (limits.length > 5 && limits[3] != 0) { + if (limits[4] > limits[5]) + settings.MakeFreeAxis(1); + else + settings.SetLimitedAxis(1, limits[4], limits[5]); + } else { + settings.MakeFreeAxis(1); + } + // Linear Z (limits[6..8]) + if (limits.length > 8 && limits[6] != 0) { + if (limits[7] > limits[8]) + settings.MakeFreeAxis(2); + else + settings.SetLimitedAxis(2, limits[7], limits[8]); + } else { + settings.MakeFreeAxis(2); + } + // Angular X (limits[9..11]) + if (limits.length > 11 && limits[9] != 0) { + if (limits[10] > limits[11]) + settings.MakeFreeAxis(3); + else + settings.SetLimitedAxis(3, limits[10], limits[11]); + } else { + settings.MakeFreeAxis(3); + } + // Angular Y (limits[12..14]) + if (limits.length > 14 && limits[12] != 0) { + if (limits[13] > limits[14]) + settings.MakeFreeAxis(4); + else + settings.SetLimitedAxis(4, limits[13], limits[14]); + } else { + settings.MakeFreeAxis(4); + } + // Angular Z (limits[15..17]) + if (limits.length > 17 && limits[15] != 0) { + if (limits[16] > limits[17]) + settings.MakeFreeAxis(5); + else + settings.SetLimitedAxis(5, limits[16], limits[17]); + } else { + settings.MakeFreeAxis(5); + } + } + + function removeFromWorld() { + if (physics != null) { + physics.removePhysicsConstraint(this); + } + } + + public function delete() { + conReady = false; + } + + public function setEnabled(enabled:Bool) { + if (conReady) { + con.SetEnabled(enabled); + } + } + + public function isEnabled():Bool { + return conReady ? con.GetEnabled() : false; + } + + // Bullet-compatible limit setting methods + public function setHingeConstraintLimits(angLimit:Bool, lowerAngLimit:Float, upperAngLimit:Float) { + if (limits == null) limits = [for (i in 0...36) 0.0]; + limits[0] = angLimit ? 1 : 0; + limits[1] = lowerAngLimit * (Math.PI / 180); + limits[2] = upperAngLimit * (Math.PI / 180); + } + + public function setSliderConstraintLimits(linLimit:Bool, lowerLinLimit:Float, upperLinLimit:Float) { + if (limits == null) limits = [for (i in 0...36) 0.0]; + limits[0] = linLimit ? 1 : 0; + limits[1] = lowerLinLimit; + limits[2] = upperLinLimit; + } + + public function setPistonConstraintLimits(linLimit:Bool, lowerLinLimit:Float, upperLinLimit:Float, angLimit:Bool, lowerAngLimit:Float, upperAngLimit:Float) { + if (limits == null) limits = [for (i in 0...36) 0.0]; + limits[0] = linLimit ? 1 : 0; + limits[1] = lowerLinLimit; + limits[2] = upperLinLimit; + limits[3] = angLimit ? 1 : 0; + limits[4] = lowerAngLimit * (Math.PI / 180); + limits[5] = upperAngLimit * (Math.PI / 180); + } + + public function setGenericConstraintLimits(setLimit:Bool = false, lowerLimit:Float = 1.0, upperLimit:Float = -1.0, axis:ConstraintAxis = X, isAngular:Bool = false) { + if (limits == null) limits = [for (i in 0...36) 0.0]; + var i = switch (axis) { + case X: 0; + case Y: 3; + case Z: 6; + }; + var j = isAngular ? 9 : 0; + var radian = isAngular ? (Math.PI / 180) : 1; + limits[i + j] = setLimit ? 1 : 0; + limits[i + j + 1] = lowerLimit * radian; + limits[i + j + 2] = upperLimit * radian; + } + + public function setSpringParams(setSpring:Bool = false, stiffness:Float = 10.0, damping:Float = 0.5, axis:ConstraintAxis = X, isAngular:Bool = false) { + if (limits == null) limits = [for (i in 0...36) 0.0]; + var i = switch (axis) { + case X: 18; + case Y: 21; + case Z: 24; + }; + var j = isAngular ? 9 : 0; + limits[i + j] = setSpring ? 1 : 0; + limits[i + j + 1] = stiffness; + limits[i + j + 2] = damping; + } +} + +@:enum abstract ConstraintAxis(Int) from Int to Int { + var X = 0; + var Y = 1; + var Z = 2; +} + +#end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsHook.hx b/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsHook.hx new file mode 100644 index 0000000..d26dc8f --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsHook.hx @@ -0,0 +1,122 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import iron.math.Vec4; +import iron.math.Mat4; +import iron.math.Quat; +import iron.Trait; +import iron.object.Object; +import iron.object.MeshObject; +import iron.object.Transform; +import iron.data.MeshData; +import iron.data.SceneFormat; + +class PhysicsHook extends Trait { + + var target:Object; + var targetName:String; + var targetTransform:Transform; + var verts:Array; + + var hookBodyId:jolt.Jt.BodyID = null; + var constraintId:Int = -1; + + static var nullvec = true; + static var vec1:jolt.Jt.Vec3; + static var quat1:jolt.Jt.Quat; + static var quat = new Quat(); + + public function new(targetName:String, verts:Array) { + super(); + + this.targetName = targetName; + this.verts = verts; + + iron.Scene.active.notifyOnInit(function() { + notifyOnInit(init); + notifyOnUpdate(update); + }); + } + + function init() { + if (nullvec) { + nullvec = false; + vec1 = new jolt.Jt.Vec3(0, 0, 0); + quat1 = new jolt.Jt.Quat(0, 0, 0, 1); + } + + target = targetName != "" ? iron.Scene.active.getChild(targetName) : null; + targetTransform = target != null ? target.transform : iron.Scene.global.transform; + + var physics = PhysicsWorld.active; + if (physics == null) + return; + + #if lnx_physics_soft + var sb:SoftBody = object.getTrait(SoftBody); + if (sb != null && sb.ready) { + // For soft body hooks, pin vertices near the target + var numVerts = Std.int(verts.length / 3); + for (j in 0...numVerts) { + var x = verts[j * 3] + sb.vertOffsetX + sb.object.transform.loc.x; + var y = verts[j * 3 + 1] + sb.vertOffsetY + sb.object.transform.loc.y; + var z = verts[j * 3 + 2] + sb.vertOffsetZ + sb.object.transform.loc.z; + + // Find and pin matching vertices + for (i in 0...@:privateAccess sb.particles.length) { + var p = @:privateAccess sb.particles[i]; + if (Math.abs(p.position.x - x) < 0.01 && Math.abs(p.position.y - y) < 0.01 && Math.abs(p.position.z - z) < 0.01) { + sb.pinVertex(i); + } + } + } + return; + } + #end + + // Rigid body hook using fixed constraint + var rb1:RigidBody = object.getTrait(RigidBody); + if (rb1 != null && rb1.ready) { + var settings = new jolt.Jt.FixedConstraintSettings(); + settings.mAutoDetectPoint = true; + var constraint = settings.Create(rb1.body, rb1.body); + physics.physicsSystem.AddConstraint(constraint); + return; + } + + // Rigid body or soft body not initialized yet + notifyOnInit(init); + } + + function update() { + #if lnx_physics_soft + // Soft body hook - update pinned vertex positions to follow target + var sb:SoftBody = object.getTrait(SoftBody); + if (sb != null && sb.ready) { + var numVerts = Std.int(verts.length / 3); + for (j in 0...numVerts) { + var x = verts[j * 3] + sb.vertOffsetX + sb.object.transform.loc.x; + var y = verts[j * 3 + 1] + sb.vertOffsetY + sb.object.transform.loc.y; + var z = verts[j * 3 + 2] + sb.vertOffsetZ + sb.object.transform.loc.z; + + // Update pinned vertex positions to target + for (i in 0...@:privateAccess sb.particles.length) { + var p = @:privateAccess sb.particles[i]; + if (p.pinned) { + // Move pinned vertex with target + var dx = targetTransform.worldx() - targetTransform.loc.x; + var dy = targetTransform.worldy() - targetTransform.loc.y; + var dz = targetTransform.worldz() - targetTransform.loc.z; + p.position.x = x + dx; + p.position.y = y + dy; + p.position.z = z + dz; + } + } + } + } + #end + } +} + +#end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsWorld.hx b/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsWorld.hx new file mode 100644 index 0000000..9fec3a7 --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/PhysicsWorld.hx @@ -0,0 +1,448 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import iron.Trait; +import iron.system.Time; +import iron.math.Vec4; +import iron.math.RayCaster; +import leenkx.trait.physics.PhysicsCache; + +class Hit { + public var rb:RigidBody; + public var pos:Vec4; + public var normal:Vec4; + + public function new(rb:RigidBody, pos:Vec4, normal:Vec4) { + this.rb = rb; + this.pos = pos; + this.normal = normal; + } +} + +class ConvexHit { + public var pos:Vec4; + public var normal:Vec4; + public var hitFraction:Float; + + public function new(pos:Vec4, normal:Vec4, hitFraction:Float) { + this.pos = pos; + this.normal = normal; + this.hitFraction = hitFraction; + } +} + +class ContactPair { + public var a:Int; + public var b:Int; + public var posA:Vec4; + public var posB:Vec4; + public var normOnB:Vec4; + public var impulse:Float; + public var distance:Float; + + public function new(a:Int, b:Int) { + this.a = a; + this.b = b; + } +} + +class PhysicsWorld extends Trait { + public static var active:PhysicsWorld = null; + static var sceneRemoved = false; + + public var physicsSystem:jolt.Jt.PhysicsSystem; + public var bodyInterface:jolt.Jt.BodyInterface; + public var physicsReady:Bool = false; + var broadPhaseOptimized:Bool = false; + + var contacts:Array; + var preUpdates:ArrayVoid> = null; + public var rbMap:Map; + public var conMap:Map; + public var constraints:Array = []; + public var timeScale = 1.0; + var maxSteps = 1; + public var solverIterations = 10; + public var hitPointWorld = new Vec4(); + public var hitNormalWorld = new Vec4(); + + // Debug drawing + var debugDrawHelper:DebugDrawHelper = null; + var debugDrawMode:DebugDrawHelper.DebugDrawMode = DebugDrawHelper.DebugDrawMode.NoDebug; + + // Jolt-specific helpers + static var nullvec = true; + static var vec1:jolt.Jt.Vec3 = null; + static var vec2:jolt.Jt.Vec3 = null; + static var quat1:jolt.Jt.Quat = null; + + #if js + var joltInterface:jolt.Jt.JoltInterface; + static var joltReady = false; + static var joltModule:Dynamic = null; + static var pendingWorlds:Array = []; + #end + + #if lnx_debug + public static var physTime = 0.0; + #end + + #if hl + @:hlNative("jolt", "Init") + static function hlJoltInit():Void {} + + @:hlNative("jolt", "Shutdown") + static function hlJoltShutdown():Void {} + #end + + public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10, fixedStep = 1 / 60) { + super(); + + if (active != null && !sceneRemoved) + return; + sceneRemoved = false; + + this.timeScale = timeScale; + this.maxSteps = maxSteps; + this.solverIterations = solverIterations; + Time.initFixedStep(fixedStep); + + contacts = []; + rbMap = new Map(); + conMap = new Map(); + active = this; + + #if js + // Check if Jolt is initialized + if (!joltReady) { + pendingWorlds.push(this); + initJolt(); + return; + } + #end + + initPhysicsWorld(); + } + + #if js + static var joltRetryRegistered = false; + static function initJolt() { + // Check if Jolt global exists (loaded via asset) + var jolt:Dynamic = untyped __js__("typeof Jolt !== 'undefined' ? Jolt : null"); + if (jolt != null) { + joltModule = jolt; + joltReady = true; + joltRetryRegistered = false; + // Initialize pending worlds + for (world in pendingWorlds) { + world.initPhysicsWorld(); + } + pendingWorlds = []; + } else if (!joltRetryRegistered) { + // Jolt not loaded yet, retry on next frame (NOT notifyOnInit which fires same-frame) + joltRetryRegistered = true; + var retryFn:Void->Void = null; + retryFn = function() { + iron.App.removeUpdate(retryFn); + joltRetryRegistered = false; + initJolt(); + }; + iron.App.notifyOnUpdate(retryFn); + } + } + #end + + function initPhysicsWorld() { + #if hl + // Must initialize Jolt allocator before ANY Jolt object creation + hlJoltInit(); + #end + + if (nullvec) { + nullvec = false; + vec1 = new jolt.Jt.Vec3(0, 0, 0); + vec2 = new jolt.Jt.Vec3(0, 0, 0); + quat1 = new jolt.Jt.Quat(0, 0, 0, 1); + } + + if (!physicsReady) { + createPhysics(); + } else if (active != null && active != this) { + physicsSystem = active.physicsSystem; + bodyInterface = active.bodyInterface; + physicsReady = true; + } + + _fixedUpdate = [fixedUpdate]; + @:privateAccess iron.App.traitFixedUpdates.insert(0, fixedUpdate); + + iron.Scene.active.notifyOnRemove(function() { + sceneRemoved = true; + PhysicsCache.clearCache(); + }); + } + + public function reset() { + for (rb in active.rbMap) + removeRigidBody(rb); + } + + function createPhysics() { + #if hl + // HashLink initialization - uses native jolt library + physicsSystem = new jolt.Jt.PhysicsSystem(); + physicsSystem.Init(10240, 0, 65536, 10240); + bodyInterface = physicsSystem.GetBodyInterface(); + #elseif js + // JavaScript/WASM initialization via JoltSettings + JoltInterface + var settings = new jolt.Jt.JoltSettings(); + settings.mMaxBodies = 10240; + settings.mMaxBodyPairs = 65536; + settings.mMaxContactConstraints = 10240; + + // Create layer interfaces + var broadPhaseLayer = new jolt.Jt.BroadPhaseLayerInterfaceTable(2, 2); + broadPhaseLayer.MapObjectToBroadPhaseLayer(0, new jolt.Jt.BroadPhaseLayer(0)); + broadPhaseLayer.MapObjectToBroadPhaseLayer(1, new jolt.Jt.BroadPhaseLayer(1)); + + var objectLayerPair = new jolt.Jt.ObjectLayerPairFilterTable(2); + objectLayerPair.EnableCollision(0, 0); + objectLayerPair.EnableCollision(0, 1); + objectLayerPair.EnableCollision(1, 1); + + var objectVsBroadPhase = new jolt.Jt.ObjectVsBroadPhaseLayerFilterTable(broadPhaseLayer, 2, objectLayerPair, 2); + + settings.mBroadPhaseLayerInterface = broadPhaseLayer; + settings.mObjectVsBroadPhaseLayerFilter = objectVsBroadPhase; + settings.mObjectLayerPairFilter = objectLayerPair; + + joltInterface = new jolt.Jt.JoltInterface(settings); + physicsSystem = joltInterface.GetPhysicsSystem(); + bodyInterface = physicsSystem.GetBodyInterface(); + #end + + physicsReady = true; + broadPhaseOptimized = false; + + var g = iron.Scene.active.raw.gravity; + var gravity = g == null ? new Vec4(0, 0, -9.81) : new Vec4(g[0], g[1], g[2]); + setGravity(gravity); + } + + public function setGravity(v:Vec4) { + vec1.Set(v.x, v.y, v.z); + physicsSystem.SetGravity(vec1); + } + + public function getGravity():Vec4 { + var g = physicsSystem.GetGravity(); + var result = new Vec4(g.GetX(), g.GetY(), g.GetZ()); + #if hl g.delete(); #end + return result; + } + + public function addRigidBody(body:RigidBody, activate:Bool = true) { + bodyInterface.AddBody(body.bodyId, activate ? 1 : 0); + rbMap.set(body.id, body); + } + + public function addPhysicsConstraint(constraint:PhysicsConstraint) { + if (constraint.conReady) { + physicsSystem.AddConstraint(constraint.con); + } + conMap.set(constraint.id, constraint); + constraints.push(constraint); + } + + public function removeRigidBody(body:RigidBody) { + if (body.destroyed) + return; + body.destroyed = true; + bodyInterface.RemoveBody(body.bodyId); + bodyInterface.DestroyBody(body.bodyId); + rbMap.remove(body.id); + } + + public function removePhysicsConstraint(constraint:PhysicsConstraint) { + if (constraint.conReady) { + physicsSystem.RemoveConstraint(constraint.con); + } + conMap.remove(constraint.id); + constraints.remove(constraint); + constraint.delete(); + } + + public function getContacts(body:RigidBody):Array { + if (contacts.length == 0) + return null; + var res:Array = []; + for (c in contacts) { + var rb:RigidBody = null; + if (c.a == body.id) + rb = rbMap.get(c.b); + else if (c.b == body.id) + rb = rbMap.get(c.a); + if (rb != null && res.indexOf(rb) == -1) + res.push(rb); + } + return res; + } + + public function getContactPairs(body:RigidBody):Array { + if (contacts.length == 0) + return null; + var res:Array = []; + for (c in contacts) { + if (c.a == body.id || c.b == body.id) + res.push(c); + } + return res; + } + + public function findBody(id:Int):RigidBody { + return rbMap.get(id); + } + + function fixedUpdate() { + var t = Time.fixedStep * timeScale * Time.scale; + if (t == 0.0) + return; + + PhysicsCache.clearContactsCache(); + + #if lnx_debug + var startTime = kha.Scheduler.realTime(); + #end + + if (preUpdates != null) + for (f in preUpdates) + f(); + + if (!broadPhaseOptimized) { + physicsSystem.OptimizeBroadPhase(); + broadPhaseOptimized = true; + } + + var currMaxSteps = t < (Time.fixedStep * (maxSteps / 10)) ? maxSteps : 1; + + #if js + joltInterface.Step(t, currMaxSteps); + #elseif hl + physicsSystem.Update(t, currMaxSteps); + #end + + for (rb in rbMap) + @:privateAccess rb.physicsUpdate(); + + #if lnx_debug + physTime = kha.Scheduler.realTime() - startTime; + #end + } + + public function pickClosest(inputX:Float, inputY:Float, group:Int = 0x00000001, mask = 0xFFFFFFFF):RigidBody { + var camera = iron.Scene.active.camera; + var start = new Vec4(); + var end = new Vec4(); + RayCaster.getDirection(start, end, inputX, inputY, camera); + var hit = rayCast(camera.transform.world.getLoc(), end, group, mask); + return hit != null ? hit.rb : null; + } + + public function rayCast(from:Vec4, to:Vec4, group:Int = 0x00000001, mask = 0xFFFFFFFF):Hit { + var dirX = to.x - from.x; + var dirY = to.y - from.y; + var dirZ = to.z - from.z; + + var origin = new jolt.Jt.RVec3(from.x, from.y, from.z); + var direction = new jolt.Jt.Vec3(dirX, dirY, dirZ); + var ray = new jolt.Jt.RRayCast(origin, direction); + var result = new jolt.Jt.RayCastResult(); + + var narrowPhase = physicsSystem.GetNarrowPhaseQuery(); + var didHit = narrowPhase.CastRay(ray, result); + + if (didHit) { + var bodyId = result.mBodyID; + var fraction = result.mFraction; + + hitPointWorld.set(from.x + dirX * fraction, from.y + dirY * fraction, from.z + dirZ * fraction); + + // Find rigid body by ID + for (rb in rbMap) { + if (rb.bodyId.GetIndex() == bodyId.GetIndex()) { + #if hl + origin.delete(); + direction.delete(); + ray.delete(); + result.delete(); + #end + return new Hit(rb, hitPointWorld.clone(), hitNormalWorld.clone()); + } + } + } + + #if hl + origin.delete(); + direction.delete(); + ray.delete(); + result.delete(); + #end + + return null; + } + + public function notifyOnPreUpdate(f:Void->Void) { + if (preUpdates == null) + preUpdates = []; + preUpdates.push(f); + } + + public function removePreUpdate(f:Void->Void) { + preUpdates.remove(f); + } + + public var convexHitPointWorld = new Vec4(); + public var convexHitNormalWorld = new Vec4(); + + public function convexSweepTest(rb:RigidBody, from:Vec4, to:Vec4, rotation:iron.math.Quat, group:Int = 0x00000001, mask = 0xFFFFFFFF):ConvexHit { + // Jolt shape cast implementation + // Note: Full shape cast requires additional Jolt bindings + // For now, use raycast as approximation + var hit = rayCast(from, to, group, mask); + if (hit != null) { + var fraction = hit.pos.sub(from).length() / to.sub(from).length(); + convexHitPointWorld = hit.pos.clone(); + convexHitNormalWorld = hit.normal.clone(); + return new ConvexHit(convexHitPointWorld, convexHitNormalWorld, fraction); + } + return null; + } + + public function setDebugDrawMode(mode:DebugDrawHelper.DebugDrawMode) { + debugDrawMode = mode; + if (mode != DebugDrawHelper.DebugDrawMode.NoDebug && debugDrawHelper == null) { + debugDrawHelper = new DebugDrawHelper(this, mode); + } else if (debugDrawHelper != null) { + debugDrawHelper.setDebugMode(mode); + } + } + + public function getDebugDrawMode():DebugDrawHelper.DebugDrawMode { + return debugDrawMode; + } + + public function debugDrawRayCast(from:Vec4, to:Vec4, hasHit:Bool, ?hitPoint:Vec4, ?hitNormal:Vec4) { + if (debugDrawHelper != null && (debugDrawMode & DebugDrawHelper.DebugDrawMode.DrawRayCast) != 0) { + debugDrawHelper.rayCast({ + from: from, + to: to, + hasHit: hasHit, + hitPoint: hitPoint, + hitNormal: hitNormal + }); + } + } +} + +#end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/RigidBody.hx b/leenkx/Sources/leenkx/trait/physics/jolt/RigidBody.hx new file mode 100644 index 0000000..430fd35 --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/RigidBody.hx @@ -0,0 +1,746 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import iron.Trait; +import iron.math.Vec4; +import iron.math.Quat; +import iron.data.SceneFormat; +import iron.object.Transform; +import iron.object.MeshObject; + +@:enum abstract Shape(Int) from Int to Int { + var Box = 0; + var Sphere = 1; + var ConvexHull = 2; + var Mesh = 3; + var Cone = 4; + var Cylinder = 5; + var Capsule = 6; + var Terrain = 7; +} + +class RigidBody extends Trait { + public var physics:PhysicsWorld; + public var transform:Transform = null; + + public var body:jolt.Jt.Body; + public var bodyId:jolt.Jt.BodyID; + + public var id:Int; + public var destroyed = false; + public var ready = false; + + public var shape:Shape; + public var mass:Float; + public var friction:Float; + public var restitution:Float; + public var group:Int; + public var mask:Int; + public var linearDamping:Float; + public var angularDamping:Float; + public var animated:Bool; + public var staticObj:Bool; + public var trigger:Bool; + var lockTranslationX:Bool; + var lockTranslationY:Bool; + var lockTranslationZ:Bool; + var lockRotationX:Bool; + var lockRotationY:Bool; + var lockRotationZ:Bool; + var ccd:Bool; + var useDeactivation:Bool; + + static var nextId = 0; + + // Native bindings for complex shape creation (HL only) + #if hl + @:hlNative("jolt", "ConvexHullShapeSettings_new") + static function hlConvexHullShapeSettings_new():Dynamic { return null; } + + @:hlNative("jolt", "ConvexHullShapeSettings_delete") + static function hlConvexHullShapeSettings_delete(settings:Dynamic):Void {} + + @:hlNative("jolt", "ConvexHullShapeSettings_AddPoint") + static function hlConvexHullShapeSettings_AddPoint(settings:Dynamic, x:Float, y:Float, z:Float):Void {} + + @:hlNative("jolt", "ConvexHullShapeSettings_Create") + static function hlConvexHullShapeSettings_Create(settings:Dynamic):Dynamic { return null; } + + @:hlNative("jolt", "MeshShapeSettings_new") + static function hlMeshShapeSettings_new():Dynamic { return null; } + + @:hlNative("jolt", "MeshShapeSettings_delete") + static function hlMeshShapeSettings_delete(settings:Dynamic):Void {} + + @:hlNative("jolt", "MeshShapeSettings_AddVertex") + static function hlMeshShapeSettings_AddVertex(settings:Dynamic, x:Float, y:Float, z:Float):Void {} + + @:hlNative("jolt", "MeshShapeSettings_AddTriangle") + static function hlMeshShapeSettings_AddTriangle(settings:Dynamic, i0:Int, i1:Int, i2:Int):Void {} + + @:hlNative("jolt", "MeshShapeSettings_Create") + static function hlMeshShapeSettings_Create(settings:Dynamic):Dynamic { return null; } + #end + + public function new(shape:Shape = Box, mass:Float = 1.0, friction:Float = 0.5, restitution:Float = 0.0, group:Int = 1, mask:Int = 1, + params:RigidBodyParams = null, flags:RigidBodyFlags = null) { + super(); + + if (params == null) params = { + linearDamping: 0.04, + angularDamping: 0.1, + angularFriction: 0.1, + linearFactorsX: 1.0, + linearFactorsY: 1.0, + linearFactorsZ: 1.0, + angularFactorsX: 1.0, + angularFactorsY: 1.0, + angularFactorsZ: 1.0, + collisionMargin: 0.0, + linearDeactivationThreshold: 0.0, + angularDeactivationThrshold: 0.0, + deactivationTime: 0.0, + linearVelocityMin: 0.0, + linearVelocityMax: 0.0, + angularVelocityMin: 0.0, + angularVelocityMax: 0.0, + lockTranslationX: false, + lockTranslationY: false, + lockTranslationZ: false, + lockRotationX: false, + lockRotationY: false, + lockRotationZ: false + }; + + if (flags == null) flags = { + animated: false, + trigger: false, + ccd: false, + interpolate: false, + staticObj: false, + useDeactivation: true + }; + + this.shape = shape; + this.mass = mass; + this.friction = friction; + this.restitution = restitution; + this.group = group; + this.mask = mask; + this.linearDamping = params.linearDamping; + this.angularDamping = params.angularDamping; + this.animated = flags.animated; + this.trigger = flags.trigger; + this.ccd = flags.ccd; + this.staticObj = flags.staticObj || mass == 0.0; + this.lockTranslationX = params.lockTranslationX; + this.lockTranslationY = params.lockTranslationY; + this.lockTranslationZ = params.lockTranslationZ; + this.lockRotationX = params.lockRotationX; + this.lockRotationY = params.lockRotationY; + this.lockRotationZ = params.lockRotationZ; + this.useDeactivation = flags.useDeactivation; + this.id = nextId++; + + notifyOnAdd(init); + notifyOnRemove(removeFromWorld); + } + + function init() { + if (ready) + return; + + transform = object.transform; + physics = PhysicsWorld.active; + + if (physics == null) { + new PhysicsWorld(); + physics = PhysicsWorld.active; + } + + #if js + // Check if Jolt is initialized - defer if not + if (!physics.physicsReady) { + // Jolt not ready yet, retry after delay + haxe.Timer.delay(init, 16); + return; + } + #end + + ready = true; + + var t = transform; + t.buildMatrix(); + var pos = t.world.getLoc(); + var rot = new Quat(); + rot.fromMat(t.world); + + // Create shape based on type - use transform.dim like Bullet does + var joltShape = createShape(t); + + // Determine motion type (0=Static, 1=Kinematic, 2=Dynamic) + var motionType:Int = staticObj ? 0 : (animated ? 1 : 2); + + // Jolt uses RVec3 for world positions + var jPos = new jolt.Jt.RVec3(pos.x, pos.y, pos.z); + var jRot = new jolt.Jt.Quat(rot.x, rot.y, rot.z, rot.w); + if (staticObj || animated) mass = 0; + + var settings = new jolt.Jt.BodyCreationSettings(joltShape, jPos, jRot, motionType, staticObj ? 0 : 1); + settings.mFriction = friction; + settings.mRestitution = restitution; + settings.mIsSensor = trigger; + + settings.mLinearDamping = linearDamping; + settings.mAngularDamping = angularDamping; + + // Match Bullet's deactivation: useDeactivation=false → DISABLE_DEACTIVATION + if (!useDeactivation) { + settings.mAllowSleeping = false; + } + + // Set mass to match Bullet (CalculateInertia = 1: use provided mass, compute inertia from shape) + // Use explicit MassProperties object to avoid chained property access issues in HL + if (mass > 0) { + settings.mOverrideMassProperties = 1; + var mp = new jolt.Jt.MassProperties(); + mp.mMass = mass; + settings.mMassPropertiesOverride = mp; + #if hl + mp.delete(); + #end + } + + // Set allowed DOFs (matching Bullet's linear/angular factors + lock properties) + var dofs = 0x3F; // All DOFs by default (0x3F = TranslationX|Y|Z|RotationX|Y|Z) + if (lockTranslationX) dofs &= ~0x01; + if (lockTranslationY) dofs &= ~0x02; + if (lockTranslationZ) dofs &= ~0x04; + if (lockRotationX) dofs &= ~0x08; + if (lockRotationY) dofs &= ~0x10; + if (lockRotationZ) dofs &= ~0x20; + if (dofs != 0x3F) settings.mAllowedDOFs = dofs; + + // CCD for fast-moving objects + if (ccd) settings.mMotionQuality = 1; // LinearCast + + body = physics.bodyInterface.CreateBody(settings); + bodyId = body.GetID(); + + #if hl + settings.delete(); + jPos.delete(); + jRot.delete(); + #end + + // Add to world (activate dynamic bodies, matching Bullet behavior) + physics.addRigidBody(this, !staticObj); + + // Initialize cached position from body creation position + currentPosX = pos.x; + currentPosY = pos.y; + currentPosZ = pos.z; + currentRotX = rot.x; + currentRotY = rot.y; + currentRotZ = rot.z; + currentRotW = rot.w; + + // Register visual update callback for non-animated bodies (matching Bullet) + if (!animated) notifyOnUpdate(update); + } + + function createShape(t:Transform):jolt.Jt.Shape { + // Use transform.dim (mesh bounding box) for shape dimensions, matching Bullet + var dimX = t.dim.x; + var dimY = t.dim.y; + var dimZ = t.dim.z; + + return switch (shape) { + case Box: + var halfExtent = new jolt.Jt.Vec3(dimX / 2, dimY / 2, dimZ / 2); + cast new jolt.Jt.BoxShape(halfExtent); + case Sphere: + var radius = dimX / 2; + cast new jolt.Jt.SphereShape(radius); + case Capsule: + var radius = dimX / 2; + var halfHeight = dimZ / 2 - radius; + if (halfHeight < 0) halfHeight = 0.01; + cast new jolt.Jt.CapsuleShape(halfHeight, radius); + case Cylinder: + var radius = Math.max(dimX, dimY) / 2; + var halfHeight = dimZ / 2; + cast new jolt.Jt.CylinderShape(halfHeight, radius); + case Cone: + var radius = Math.max(dimX, dimY) / 2; + var halfHeight = dimZ / 2; + cast new jolt.Jt.CylinderShape(halfHeight, radius); + case ConvexHull: + #if hl + createConvexHullShape(t.scale); + #else + createConvexHullShapeJS(t.scale, dimX, dimY, dimZ); + #end + case Mesh: + // Jolt MeshShape only works for static bodies (unlike Bullet's GImpact) + // Dynamic mesh bodies must use ConvexHullShape instead + if (staticObj) { + #if hl + createMeshShape(t.scale); + #else + createMeshShapeJS(t.scale, dimX, dimY, dimZ); + #end + } else { + #if hl + createConvexHullShape(t.scale); + #else + createConvexHullShapeJS(t.scale, dimX, dimY, dimZ); + #end + } + case Terrain: + #if hl + createTerrainShape(t.scale); + #else + createMeshShapeJS(t.scale, dimX, dimY, dimZ); + #end + default: + var halfExtent = new jolt.Jt.Vec3(dimX / 2, dimY / 2, dimZ / 2); + cast new jolt.Jt.BoxShape(halfExtent); + }; + } + + function createConvexHullShape(scale:Vec4):jolt.Jt.Shape { + var mo = cast(object, MeshObject); + if (mo == null || mo.data == null || mo.data.geom == null) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + } + + var positions = mo.data.geom.positions.values; + var scalePos = mo.data.scalePos; + + #if hl + var settings = hlConvexHullShapeSettings_new(); + var numVerts = Std.int(positions.length / 4); + for (i in 0...numVerts) { + var x = (positions[i * 4] / 32767) * scalePos * scale.x; + var y = (positions[i * 4 + 1] / 32767) * scalePos * scale.y; + var z = (positions[i * 4 + 2] / 32767) * scalePos * scale.z; + hlConvexHullShapeSettings_AddPoint(settings, x, y, z); + } + var shape = hlConvexHullShapeSettings_Create(settings); + hlConvexHullShapeSettings_delete(settings); + if (shape == null) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + } + return cast shape; + #else + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + #end + } + + function createMeshShape(scale:Vec4):jolt.Jt.Shape { + var mo = cast(object, MeshObject); + if (mo == null || mo.data == null || mo.data.geom == null) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + } + + var positions = mo.data.geom.positions.values; + var indices = mo.data.geom.indices; + var scalePos = mo.data.scalePos; + + #if hl + var settings = hlMeshShapeSettings_new(); + var numVerts = Std.int(positions.length / 4); + + for (i in 0...numVerts) { + var x = (positions[i * 4] / 32767) * scalePos * scale.x; + var y = (positions[i * 4 + 1] / 32767) * scalePos * scale.y; + var z = (positions[i * 4 + 2] / 32767) * scalePos * scale.z; + hlMeshShapeSettings_AddVertex(settings, x, y, z); + } + + for (indexArray in indices) { + var numTris = Std.int(indexArray.length / 3); + for (i in 0...numTris) { + hlMeshShapeSettings_AddTriangle(settings, indexArray[i * 3], indexArray[i * 3 + 1], indexArray[i * 3 + 2]); + } + } + + var shape = hlMeshShapeSettings_Create(settings); + hlMeshShapeSettings_delete(settings); + if (shape == null) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + } + return cast shape; + #else + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + #end + } + + function createTerrainShape(scale:Vec4):jolt.Jt.Shape { + // Terrain/HeightField shape - requires height data from object + var mo = cast(object, MeshObject); + if (mo == null) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + } + + #if js + // For JS, use HeightFieldShapeSettings or fallback to mesh + // Terrain meshes are typically treated as mesh shapes in Jolt + return createMeshShape(scale); + #elseif hl + // For HashLink, terrain is also best represented as mesh shape + // HeightFieldShape requires specific grid data which terrain meshes may not have + return createMeshShape(scale); + #else + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5)); + #end + } + + #if js + function createConvexHullShapeJS(scale:Vec4, dimX:Float, dimY:Float, dimZ:Float):jolt.Jt.Shape { + var mo = cast(object, MeshObject); + if (mo == null || mo.data == null || mo.data.geom == null) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(dimX / 2, dimY / 2, dimZ / 2)); + } + + var positions = mo.data.geom.positions.values; + var scalePos = mo.data.scalePos; + var numVerts = Std.int(positions.length / 4); + + var settings:Dynamic = untyped __js__("new Jolt.ConvexHullShapeSettings()"); + var points:Dynamic = untyped settings.mPoints; + points.clear(); + for (i in 0...numVerts) { + var x:Float = (positions[i * 4] / 32767) * scalePos * scale.x; + var y:Float = (positions[i * 4 + 1] / 32767) * scalePos * scale.y; + var z:Float = (positions[i * 4 + 2] / 32767) * scalePos * scale.z; + var pt:Dynamic = untyped __js__("new Jolt.Vec3({0}, {1}, {2})", x, y, z); + untyped points.push_back(pt); + } + + var result:Dynamic = untyped settings.Create(); + if (untyped result.HasError()) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(dimX / 2, dimY / 2, dimZ / 2)); + } + return cast untyped result.Get(); + } + + function createMeshShapeJS(scale:Vec4, dimX:Float, dimY:Float, dimZ:Float):jolt.Jt.Shape { + var mo = cast(object, MeshObject); + if (mo == null || mo.data == null || mo.data.geom == null) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(dimX / 2, dimY / 2, dimZ / 2)); + } + + var positions = mo.data.geom.positions.values; + var indices = mo.data.geom.indices; + var scalePos = mo.data.scalePos; + var numVerts = Std.int(positions.length / 4); + + var settings:Dynamic = untyped __js__("new Jolt.MeshShapeSettings()"); + var verts:Dynamic = untyped settings.mTriangleVertices; + var tris:Dynamic = untyped settings.mIndexedTriangles; + verts.clear(); + tris.clear(); + + for (i in 0...numVerts) { + var x:Float = (positions[i * 4] / 32767) * scalePos * scale.x; + var y:Float = (positions[i * 4 + 1] / 32767) * scalePos * scale.y; + var z:Float = (positions[i * 4 + 2] / 32767) * scalePos * scale.z; + var v:Dynamic = untyped __js__("new Jolt.Float3({0}, {1}, {2})", x, y, z); + untyped verts.push_back(v); + } + + for (indexArray in indices) { + var numTris = Std.int(indexArray.length / 3); + for (i in 0...numTris) { + var tri:Dynamic = untyped __js__("new Jolt.IndexedTriangle()"); + untyped tri.set_mIdx(0, indexArray[i * 3]); + untyped tri.set_mIdx(1, indexArray[i * 3 + 1]); + untyped tri.set_mIdx(2, indexArray[i * 3 + 2]); + untyped tri.set_mMaterialIndex(0); + untyped tris.push_back(tri); + } + } + + var result:Dynamic = untyped settings.Create(); + if (untyped result.HasError()) { + return cast new jolt.Jt.BoxShape(new jolt.Jt.Vec3(dimX / 2, dimY / 2, dimZ / 2)); + } + return cast untyped result.Get(); + } + #end + + // Cached physics state for visual interpolation (matching Bullet pattern) + var currentPosX:Float = 0; + var currentPosY:Float = 0; + var currentPosZ:Float = 0; + var currentRotX:Float = 0; + var currentRotY:Float = 0; + var currentRotZ:Float = 0; + var currentRotW:Float = 1; + + public function physicsUpdate() { + if (!ready) + return; + + if (staticObj) + return; + + if (animated) { + syncTransform(); + return; + } + + var active = physics.bodyInterface.IsActive(bodyId); + if (!active) + return; + + // Read position and rotation from Jolt into cached state + var p = physics.bodyInterface.GetPosition(bodyId); + var q = physics.bodyInterface.GetRotation(bodyId); + + #if js + currentPosX = cast p.GetX(); + currentPosY = cast p.GetY(); + currentPosZ = cast p.GetZ(); + currentRotX = cast q.GetX(); + currentRotY = cast q.GetY(); + currentRotZ = cast q.GetZ(); + currentRotW = cast q.GetW(); + // JS: getter return values use internal WASM wrappers - do NOT destroy + #else + currentPosX = p.GetX(); + currentPosY = p.GetY(); + currentPosZ = p.GetZ(); + currentRotX = q.GetX(); + currentRotY = q.GetY(); + currentRotZ = q.GetZ(); + currentRotW = q.GetW(); + p.delete(); + q.delete(); + #end + } + + function update() { + transform.loc.set(currentPosX, currentPosY, currentPosZ); + transform.rot.set(currentRotX, currentRotY, currentRotZ, currentRotW); + + if (object.parent != null) { + var ptransform = object.parent.transform; + transform.loc.x -= ptransform.worldx(); + transform.loc.y -= ptransform.worldy(); + transform.loc.z -= ptransform.worldz(); + } + + transform.buildMatrix(); + } + + function removeFromWorld() { + if (physics != null) { + physics.removeRigidBody(this); + } + } + + public function delete() { + // Cleanup handled by physics world + } + + // Physics methods + public function applyForce(force:Vec4, ?loc:Vec4) { + activate(); + if (loc == null) { + var f = new jolt.Jt.Vec3(force.x, force.y, force.z); + physics.bodyInterface.AddForce(bodyId, f); + #if hl f.delete(); #end + } else { + var f = new jolt.Jt.Vec3(force.x, force.y, force.z); + var l = new jolt.Jt.RVec3(loc.x, loc.y, loc.z); + physics.bodyInterface.AddForceAtPosition(bodyId, f, l); + #if hl f.delete(); l.delete(); #end + } + } + + public function applyImpulse(impulse:Vec4, ?loc:Vec4) { + activate(); + if (loc == null) { + var i = new jolt.Jt.Vec3(impulse.x, impulse.y, impulse.z); + physics.bodyInterface.AddImpulse(bodyId, i); + #if hl i.delete(); #end + } else { + var i = new jolt.Jt.Vec3(impulse.x, impulse.y, impulse.z); + var l = new jolt.Jt.RVec3(loc.x, loc.y, loc.z); + physics.bodyInterface.AddImpulseAtPosition(bodyId, i, l); + #if hl i.delete(); l.delete(); #end + } + } + + public function applyTorque(torque:Vec4) { + activate(); + var t = new jolt.Jt.Vec3(torque.x, torque.y, torque.z); + physics.bodyInterface.AddTorque(bodyId, t); + #if hl t.delete(); #end + } + + public function applyTorqueImpulse(impulse:Vec4) { + activate(); + var i = new jolt.Jt.Vec3(impulse.x, impulse.y, impulse.z); + physics.bodyInterface.AddAngularImpulse(bodyId, i); + #if hl i.delete(); #end + } + + public function setLinearVelocity(v:Vec4) { + var vel = new jolt.Jt.Vec3(v.x, v.y, v.z); + physics.bodyInterface.SetLinearVelocity(bodyId, vel); + #if hl vel.delete(); #end + } + + public function getLinearVelocity():Vec4 { + var v = physics.bodyInterface.GetLinearVelocity(bodyId); + var result = new Vec4(v.GetX(), v.GetY(), v.GetZ()); + #if hl v.delete(); #end + return result; + } + + public function setAngularVelocity(v:Vec4) { + var vel = new jolt.Jt.Vec3(v.x, v.y, v.z); + physics.bodyInterface.SetAngularVelocity(bodyId, vel); + #if hl vel.delete(); #end + } + + public function getAngularVelocity():Vec4 { + var v = physics.bodyInterface.GetAngularVelocity(bodyId); + var result = new Vec4(v.GetX(), v.GetY(), v.GetZ()); + #if hl v.delete(); #end + return result; + } + + public function setFriction(f:Float) { + friction = f; + physics.bodyInterface.SetFriction(bodyId, f); + } + + public function setRestitution(r:Float) { + restitution = r; + physics.bodyInterface.SetRestitution(bodyId, r); + } + + public function setGravityFactor(f:Float) { + physics.bodyInterface.SetGravityFactor(bodyId, f); + } + + public function activate() { + physics.bodyInterface.ActivateBody(bodyId); + } + + public function disableSimulation() { + physics.bodyInterface.DeactivateBody(bodyId); + } + + public function setPosition(pos:Vec4) { + var p = new jolt.Jt.RVec3(pos.x, pos.y, pos.z); + physics.bodyInterface.SetPosition(bodyId, p, 0); + #if hl p.delete(); #end + } + + public function setRotation(rot:Quat) { + var q = new jolt.Jt.Quat(rot.x, rot.y, rot.z, rot.w); + physics.bodyInterface.SetRotation(bodyId, q, 0); + #if hl q.delete(); #end + } + + public function syncTransform() { + var t = transform; + var pos = t.world.getLoc(); + var rot = new Quat(); + rot.fromMat(t.world); + setPosition(pos); + setRotation(rot); + } + + public function isActive():Bool { + return physics.bodyInterface.IsActive(bodyId); + } + + public function disableGravity() { + setGravityFactor(0.0); + } + + public function enableGravity() { + setGravityFactor(1.0); + } + + public function getPointVelocity(x:Float, y:Float, z:Float):Vec4 { + var linear = getLinearVelocity(); + var relativePoint = new Vec4(x, y, z).sub(transform.world.getLoc()); + var angular = getAngularVelocity().cross(relativePoint); + return linear.add(angular); + } + + public function disableCollision() { + // In Jolt, use SetIsSensor to disable contact response + body.SetIsSensor(true); + } + + public function enableCollision() { + body.SetIsSensor(false); + } + + public function notifyOnContact(f:RigidBody->Void) { + if (onContact == null) + onContact = []; + onContact.push(f); + } + + public function removeContact(f:RigidBody->Void) { + if (onContact != null) + onContact.remove(f); + } + + public var onContact:ArrayVoid> = null; + public var onReady:Void->Void = null; + + public function notifyOnReady(f:Void->Void) { + onReady = f; + if (ready) + onReady(); + } +} + +typedef RigidBodyParams = { + var linearDamping:Float; + var angularDamping:Float; + var angularFriction:Float; + var linearFactorsX:Float; + var linearFactorsY:Float; + var linearFactorsZ:Float; + var angularFactorsX:Float; + var angularFactorsY:Float; + var angularFactorsZ:Float; + var collisionMargin:Float; + var linearDeactivationThreshold:Float; + var angularDeactivationThrshold:Float; + var deactivationTime:Float; + var linearVelocityMin:Float; + var linearVelocityMax:Float; + var angularVelocityMin:Float; + var angularVelocityMax:Float; + var lockTranslationX:Bool; + var lockTranslationY:Bool; + var lockTranslationZ:Bool; + var lockRotationX:Bool; + var lockRotationY:Bool; + var lockRotationZ:Bool; +} + +typedef RigidBodyFlags = { + var animated:Bool; + var trigger:Bool; + var ccd:Bool; + var interpolate:Bool; + var staticObj:Bool; + var useDeactivation:Bool; +} + +#end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/SoftBody.hx b/leenkx/Sources/leenkx/trait/physics/jolt/SoftBody.hx new file mode 100644 index 0000000..4301aef --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/SoftBody.hx @@ -0,0 +1,528 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import iron.Trait; +import iron.math.Vec4; +import iron.object.MeshObject; +import iron.data.MeshData; +import iron.data.SceneFormat; +import iron.system.Time; +import kha.arrays.ByteArray; + +@:enum abstract SoftShape(Int) from Int { + var Cloth = 0; + var Volume = 1; +} + +// Soft body particle for spring-mass simulation +class SoftParticle { + public var position:Vec4; + public var velocity:Vec4; + public var acceleration:Vec4; + public var invMass:Float; + public var pinned:Bool; + + public function new(x:Float, y:Float, z:Float, mass:Float) { + position = new Vec4(x, y, z); + velocity = new Vec4(0, 0, 0); + acceleration = new Vec4(0, 0, 0); + invMass = mass > 0 ? 1.0 / mass : 0; + pinned = false; + } + + public function applyForce(force:Vec4) { + acceleration.x += force.x * invMass; + acceleration.y += force.y * invMass; + acceleration.z += force.z * invMass; + } + + public function integrate(dt:Float) { + if (pinned || invMass == 0) + return; + + velocity.x += acceleration.x * dt; + velocity.y += acceleration.y * dt; + velocity.z += acceleration.z * dt; + + // Damping + velocity.x *= 0.99; + velocity.y *= 0.99; + velocity.z *= 0.99; + + position.x += velocity.x * dt; + position.y += velocity.y * dt; + position.z += velocity.z * dt; + + acceleration.set(0, 0, 0); + } +} + +// Spring constraint between particles +class SoftSpring { + public var p1:Int; + public var p2:Int; + public var restLength:Float; + public var stiffness:Float; + + public function new(p1:Int, p2:Int, restLength:Float, stiffness:Float) { + this.p1 = p1; + this.p2 = p2; + this.restLength = restLength; + this.stiffness = stiffness; + } +} + +class SoftBody extends Trait { + + static var physics:PhysicsWorld = null; + + public var ready = false; + var shape:SoftShape; + var bend:Float; + var mass:Float; + var margin:Float; + + public var vertOffsetX = 0.0; + public var vertOffsetY = 0.0; + public var vertOffsetZ = 0.0; + + // Spring-mass simulation + var particles:Array = []; + var springs:Array = []; + var gravity:Vec4; + var iterations = 3; + + // Mesh data for vertex updates + var meshObject:MeshObject; + + // Vertex deduplication: maps unique vertex index → list of raw vertex indices + // Meshes split vertices for normals/UVs, but soft body particles must be unique + var vertexIndexMap:Map>; + + public function new(shape = SoftShape.Cloth, bend = 0.5, mass = 1.0, margin = 0.04) { + super(); + this.shape = shape; + this.bend = bend; + this.mass = mass; + this.margin = margin; + + notifyOnInit(init); + } + + function init() { + var mo = cast(object, MeshObject); + new MeshData(mo.data.raw, function(data) { + mo.setData(data); + initSoftBody(); + }); + } + + function retryInit() { + iron.App.removeUpdate(retryInit); + initSoftBody(); + } + + function initSoftBody() { + if (ready) + return; + + if (PhysicsWorld.active == null || !PhysicsWorld.active.physicsReady) { + iron.App.notifyOnUpdate(retryInit); + return; + } + + ready = true; + + if (physics == null) + physics = PhysicsWorld.active; + + meshObject = cast(object, MeshObject); + meshObject.frustumCulling = false; + var geom = meshObject.data.geom; + var rawData = meshObject.data.raw; + + // Get gravity from physics world + var g = physics.getGravity(); + gravity = new Vec4(g.x, g.y, g.z); + + // Parented soft body - clear parent location + if (object.parent != null && object.parent.name != "") { + object.transform.loc.x += object.parent.transform.worldx(); + object.transform.loc.y += object.parent.transform.worldy(); + object.transform.loc.z += object.parent.transform.worldz(); + object.transform.localOnly = true; + object.transform.buildMatrix(); + } + + // Build vertex deduplication map from vertex_map (matching Bullet SoftBody pattern) + // vertex_map[rawVertexBufferIdx] = uniqueVertexIdx + // vertex_map.length = number of raw vertices in vertex buffer (NOT same as geom.indices length) + // vertexIndexMap: uniqueVertexIdx → [rawVertexBufferIdx, ...] + vertexIndexMap = new Map(); + var hasVertexMap = false; + for (ind in rawData.index_arrays) { + if (ind.vertex_map != null) { + hasVertexMap = true; + for (rawIdx in 0...ind.vertex_map.length) { + var uniqueIdx = ind.vertex_map[rawIdx]; + var mapping = vertexIndexMap.get(uniqueIdx); + if (mapping == null) { + vertexIndexMap.set(uniqueIdx, [rawIdx]); + } else { + if (!mapping.contains(rawIdx)) + mapping.push(rawIdx); + } + } + } + } + + var positions = geom.positions.values; + var scalePos = meshObject.data.scalePos; + + if (hasVertexMap) { + // Create particles only for unique vertices + var numUnique = 0; + for (_ in vertexIndexMap.keys()) + numUnique++; + + for (key in 0...numUnique) { + var rawIndices = vertexIndexMap.get(key); + if (rawIndices == null || rawIndices.length == 0) + continue; + var ri = rawIndices[0]; + var x = (positions[ri * 4] / 32767) * scalePos; + var y = (positions[ri * 4 + 1] / 32767) * scalePos; + var z = (positions[ri * 4 + 2] / 32767) * scalePos; + + // Apply object rotation and scale + var vt = new Vec4(x, y, z); + vt.applyQuat(object.transform.rot); + vt.x *= object.transform.scale.x; + vt.y *= object.transform.scale.y; + vt.z *= object.transform.scale.z; + vt.addf(object.transform.worldx(), object.transform.worldy(), object.transform.worldz()); + + var particle = new SoftParticle(vt.x, vt.y, vt.z, mass / numUnique); + particles.push(particle); + } + + // Create springs from triangle connectivity + // geom.indices[ia] = triangle index list of raw vertex buffer indices + // vertex_map[rawIdx] = unique vertex index for that raw vertex + var createdSprings:Map = new Map(); + var indexArrayIdx = 0; + for (ind in rawData.index_arrays) { + if (ind.vertex_map == null) { + indexArrayIdx++; + continue; + } + var indexArray = geom.indices[indexArrayIdx]; + var numTris = Std.int(indexArray.length / 3); + for (i in 0...numTris) { + var r0 = indexArray[i * 3]; + var r1 = indexArray[i * 3 + 1]; + var r2 = indexArray[i * 3 + 2]; + // Map raw vertex buffer indices → unique vertex indices + var u0 = ind.vertex_map[r0]; + var u1 = ind.vertex_map[r1]; + var u2 = ind.vertex_map[r2]; + + createSpring(u0, u1, createdSprings); + createSpring(u1, u2, createdSprings); + createSpring(u2, u0, createdSprings); + } + indexArrayIdx++; + } + } else { + // Fallback: no vertex_map, treat each vertex as unique + var numVerts = Std.int(positions.length / 4); + vertexIndexMap = new Map(); + for (i in 0...numVerts) { + vertexIndexMap.set(i, [i]); + + var x = (positions[i * 4] / 32767) * scalePos; + var y = (positions[i * 4 + 1] / 32767) * scalePos; + var z = (positions[i * 4 + 2] / 32767) * scalePos; + + var vt = new Vec4(x, y, z); + vt.applyQuat(object.transform.rot); + vt.x *= object.transform.scale.x; + vt.y *= object.transform.scale.y; + vt.z *= object.transform.scale.z; + vt.addf(object.transform.worldx(), object.transform.worldy(), object.transform.worldz()); + + var particle = new SoftParticle(vt.x, vt.y, vt.z, mass / numVerts); + particles.push(particle); + } + + var createdSprings:Map = new Map(); + for (indexArray in geom.indices) { + var numTris = Std.int(indexArray.length / 3); + for (i in 0...numTris) { + createSpring(indexArray[i * 3], indexArray[i * 3 + 1], createdSprings); + createSpring(indexArray[i * 3 + 1], indexArray[i * 3 + 2], createdSprings); + createSpring(indexArray[i * 3 + 2], indexArray[i * 3], createdSprings); + } + } + } + + // Pin top edge vertices for cloth simulation + if (shape == SoftShape.Cloth) { + var minX = Math.POSITIVE_INFINITY, maxX = Math.NEGATIVE_INFINITY; + var minY = Math.POSITIVE_INFINITY, maxY = Math.NEGATIVE_INFINITY; + var minZ = Math.POSITIVE_INFINITY, maxZ = Math.NEGATIVE_INFINITY; + for (p in particles) { + if (p.position.x < minX) minX = p.position.x; + if (p.position.x > maxX) maxX = p.position.x; + if (p.position.y < minY) minY = p.position.y; + if (p.position.y > maxY) maxY = p.position.y; + if (p.position.z < minZ) minZ = p.position.z; + if (p.position.z > maxZ) maxZ = p.position.z; + } + var extX = maxX - minX; + var extY = maxY - minY; + var extZ = maxZ - minZ; + + if (extZ > 0.01) { + var threshold = maxZ - extZ * 0.05; + for (p in particles) { + if (p.position.z >= threshold) + p.pinned = true; + } + } else if (extY > 0.01) { + var threshold = maxY - extY * 0.05; + for (p in particles) { + if (p.position.y >= threshold) + p.pinned = true; + } + } else if (extX > 0.01) { + var threshold = maxX - extX * 0.05; + for (p in particles) { + if (p.position.x >= threshold) + p.pinned = true; + } + } + } + + notifyOnRemove(removeFromWorld); + notifyOnUpdate(update); + } + + function createSpring(i0:Int, i1:Int, createdSprings:Map) { + if (i0 == i1) + return; + var key = i0 < i1 ? '${i0}_${i1}' : '${i1}_${i0}'; + if (createdSprings.exists(key)) + return; + createdSprings.set(key, true); + + if (i0 >= particles.length || i1 >= particles.length) + return; + + var p0 = particles[i0]; + var p1 = particles[i1]; + var dx = p1.position.x - p0.position.x; + var dy = p1.position.y - p0.position.y; + var dz = p1.position.z - p0.position.z; + var restLength = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (restLength < 0.0001) + return; + + springs.push(new SoftSpring(i0, i1, restLength, 1.0 - bend)); + } + + function update() { + var dt = Time.delta; + if (dt <= 0 || dt > 0.1) + return; + + // Apply gravity as acceleration (m/s^2, independent of mass) + for (p in particles) { + if (!p.pinned) { + p.acceleration.x += gravity.x; + p.acceleration.y += gravity.y; + p.acceleration.z += gravity.z; + } + } + + // Integrate particles + for (p in particles) { + p.integrate(dt); + } + + // Solve spring constraints + for (iter in 0...iterations) { + for (spring in springs) { + solveSpring(spring); + } + } + + // Ground collision + for (p in particles) { + if (p.position.z < 0) { + p.position.z = 0; + p.velocity.z = 0; + } + } + + // Update mesh vertices + updateMeshVertices(); + } + + function solveSpring(spring:SoftSpring) { + var p1 = particles[spring.p1]; + var p2 = particles[spring.p2]; + + var dx = p2.position.x - p1.position.x; + var dy = p2.position.y - p1.position.y; + var dz = p2.position.z - p1.position.z; + var dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist < 0.0001) + return; + + var diff = (dist - spring.restLength) / dist; + var stiffness = spring.stiffness; + var correction = diff * stiffness * 0.5; + + if (!p1.pinned && p1.invMass > 0) { + p1.position.x += dx * correction; + p1.position.y += dy * correction; + p1.position.z += dz * correction; + } + + if (!p2.pinned && p2.invMass > 0) { + p2.position.x -= dx * correction; + p2.position.y -= dy * correction; + p2.position.z -= dz * correction; + } + } + + function updateMeshVertices() { + var geom = meshObject.data.geom; + var numNodes = particles.length; + + // Compute mean position (center of mass) for object placement + vertOffsetX = 0.0; + vertOffsetY = 0.0; + vertOffsetZ = 0.0; + for (p in particles) { + vertOffsetX += p.position.x; + vertOffsetY += p.position.y; + vertOffsetZ += p.position.z; + } + vertOffsetX /= numNodes; + vertOffsetY /= numNodes; + vertOffsetZ /= numNodes; + + // Set object transform to center of mass + meshObject.transform.scale.set(1, 1, 1); + meshObject.transform.loc.set(vertOffsetX, vertOffsetY, vertOffsetZ); + meshObject.transform.rot.set(0, 0, 0, 1); + + // Compute scalePos to fit all vertices + var scalePos = 1.0; + for (p in particles) { + var mx = Math.abs((p.position.x - vertOffsetX) * 2); + var my = Math.abs((p.position.y - vertOffsetY) * 2); + var mz = Math.abs((p.position.z - vertOffsetZ) * 2); + if (mx > scalePos) scalePos = mx; + if (my > scalePos) scalePos = my; + if (mz > scalePos) scalePos = mz; + } + + meshObject.data.scalePos = scalePos; + meshObject.transform.scaleWorld = scalePos; + meshObject.transform.buildMatrix(); + + var invScalePos = 1.0 / scalePos; + + // Lock vertex buffer(s) for GPU upload + #if lnx_deinterleaved + var v:ByteArray = geom.vertexBuffers[0].buffer.lock(); + #else + var v:ByteArray = geom.vertexBuffer.lock(); + var vbPos = geom.vertexBufferMap.get("pos"); + var v2 = vbPos != null ? vbPos.lock() : null; + var l = geom.structLength; + #end + + // Write each unique particle position to all its raw vertex copies + for (uniqueIdx in 0...numNodes) { + var p = particles[uniqueIdx]; + var indices = vertexIndexMap.get(uniqueIdx); + if (indices == null) + continue; + + var mx = p.position.x - vertOffsetX; + var my = p.position.y - vertOffsetY; + var mz = p.position.z - vertOffsetZ; + + var sx = Std.int(mx * 32767 * invScalePos); + var sy = Std.int(my * 32767 * invScalePos); + var sz = Std.int(mz * 32767 * invScalePos); + + for (idx in indices) { + #if lnx_deinterleaved + v.setInt16(idx * 8, sx); + v.setInt16(idx * 8 + 2, sy); + v.setInt16(idx * 8 + 4, sz); + #else + var vertIndex = idx * l * 2; + v.setInt16(vertIndex, sx); + v.setInt16(vertIndex + 2, sy); + v.setInt16(vertIndex + 4, sz); + if (v2 != null) { + v2.setInt16(idx * 8, sx); + v2.setInt16(idx * 8 + 2, sy); + v2.setInt16(idx * 8 + 4, sz); + } + #end + } + } + + // Unlock triggers GPU upload + #if lnx_deinterleaved + geom.vertexBuffers[0].buffer.unlock(); + #else + geom.vertexBuffer.unlock(); + if (vbPos != null) vbPos.unlock(); + #end + } + + public function pinVertex(index:Int) { + if (index >= 0 && index < particles.length) { + particles[index].pinned = true; + } + } + + public function unpinVertex(index:Int) { + if (index >= 0 && index < particles.length) { + particles[index].pinned = false; + } + } + + public function applyForceToVertex(index:Int, force:Vec4) { + if (index >= 0 && index < particles.length) { + particles[index].applyForce(force); + } + } + + public function applyWindForce(direction:Vec4, strength:Float) { + var wind = new Vec4(direction.x * strength, direction.y * strength, direction.z * strength); + for (p in particles) { + if (!p.pinned) { + p.applyForce(wind); + } + } + } + + function removeFromWorld() { + particles = []; + springs = []; + } +} + +#end diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/VehicleBody.hx b/leenkx/Sources/leenkx/trait/physics/jolt/VehicleBody.hx new file mode 100644 index 0000000..5eb092d --- /dev/null +++ b/leenkx/Sources/leenkx/trait/physics/jolt/VehicleBody.hx @@ -0,0 +1,276 @@ +package leenkx.trait.physics.jolt; + +#if lnx_jolt + +import iron.Trait; +import iron.object.Object; +import iron.object.CameraObject; +import iron.object.Transform; +import iron.system.Time; +import iron.math.Vec4; + +class VehicleBody extends Trait { + + @prop var wheel0Name:String = "Wheel0"; + @prop var wheel1Name:String = "Wheel1"; + @prop var wheel2Name:String = "Wheel2"; + @prop var wheel3Name:String = "Wheel3"; + + @prop var chassisMass:Float = 1500.0; + @prop var maxEngineForce:Float = 3000.0; + @prop var maxBrakingForce:Float = 500.0; + @prop var maxSteeringAngle:Float = 0.5; + @prop var suspensionMinLength:Float = 0.1; + @prop var suspensionMaxLength:Float = 0.5; + @prop var wheelRadius:Float = 0.3; + + var physics:PhysicsWorld; + var transform:Transform; + var camera:CameraObject; + + var wheels:Array = []; + var wheelInfos:Array = []; + var chassisBody:RigidBody; + var chassisBodyId:jolt.Jt.BodyID; + var chassisReady:Bool = false; + + var engineForce = 0.0; + var brakingForce = 0.0; + var vehicleSteering = 0.0; + var currentSpeed = 0.0; + + public function new() { + super(); + iron.Scene.active.notifyOnInit(init); + } + + function init() { + physics = PhysicsWorld.active; + transform = object.transform; + camera = iron.Scene.active.camera; + + // Collect wheel objects + for (n in [wheel0Name, wheel1Name, wheel2Name, wheel3Name]) { + var wheel = iron.Scene.active.root.getChild(n); + if (wheel != null) { + wheels.push(wheel); + wheelInfos.push(new VehicleWheel(wheelInfos.length, wheel.transform, transform)); + } + } + + // Create chassis rigid body + var pos = transform.world.getLoc(); + var rot = new iron.math.Quat(); + rot.fromMat(transform.world); + + var halfExtent = new jolt.Jt.Vec3(transform.dim.x / 2, transform.dim.y / 2, transform.dim.z / 4); + var chassisShape = new jolt.Jt.BoxShape(halfExtent); + + var jPos = new jolt.Jt.RVec3(pos.x, pos.y, pos.z); + var jRot = new jolt.Jt.Quat(rot.x, rot.y, rot.z, rot.w); + + var settings = new jolt.Jt.BodyCreationSettings(chassisShape, jPos, jRot, 2, 1); + settings.mFriction = 0.6; + settings.mRestitution = 0.1; + settings.mLinearDamping = 0.1; + settings.mAngularDamping = 0.5; + settings.mGravityFactor = 1.0; + + var body = physics.bodyInterface.CreateBody(settings); + chassisBodyId = body.GetID(); + + #if hl + settings.delete(); + jPos.delete(); + jRot.delete(); + #end + + physics.bodyInterface.AddBody(chassisBodyId, 1); + chassisReady = true; + + notifyOnUpdate(update); + notifyOnRemove(onRemove); + } + + function onRemove() { + if (chassisReady) { + physics.bodyInterface.RemoveBody(chassisBodyId); + physics.bodyInterface.DestroyBody(chassisBodyId); + } + } + + function update() { + var keyboard = iron.system.Input.getKeyboard(); + var forward = keyboard.down(keyUp); + var backward = keyboard.down(keyDown); + var left = keyboard.down(keyLeft); + var right = keyboard.down(keyRight); + var brake = keyboard.down("space"); + + // Engine force + if (forward) { + engineForce = maxEngineForce; + brakingForce = 0; + } else if (backward) { + engineForce = -maxEngineForce * 0.5; + brakingForce = 0; + } else if (brake) { + engineForce = 0; + brakingForce = maxBrakingForce; + } else { + engineForce = 0; + brakingForce = 20; // Rolling resistance + } + + // Steering with smooth interpolation + var steerSpeed = 2.0 * Time.step; + if (left) { + vehicleSteering = Math.min(vehicleSteering + steerSpeed, maxSteeringAngle); + } else if (right) { + vehicleSteering = Math.max(vehicleSteering - steerSpeed, -maxSteeringAngle); + } else { + // Return to center + if (vehicleSteering > 0) { + vehicleSteering = Math.max(0, vehicleSteering - steerSpeed * 2); + } else if (vehicleSteering < 0) { + vehicleSteering = Math.min(0, vehicleSteering + steerSpeed * 2); + } + } + + if (chassisReady) { + // Get current velocity to calculate speed + var vel = physics.bodyInterface.GetLinearVelocity(chassisBodyId); + currentSpeed = Math.sqrt(vel.GetX() * vel.GetX() + vel.GetY() * vel.GetY() + vel.GetZ() * vel.GetZ()); + + // Get chassis orientation + var chassisRot = physics.bodyInterface.GetRotation(chassisBodyId); + var chassisPos = physics.bodyInterface.GetPosition(chassisBodyId); + + // Calculate forward direction from chassis rotation + var forwardX = 2.0 * (chassisRot.GetX() * chassisRot.GetZ() + chassisRot.GetW() * chassisRot.GetY()); + var forwardY = 2.0 * (chassisRot.GetY() * chassisRot.GetZ() - chassisRot.GetW() * chassisRot.GetX()); + var forwardZ = 1.0 - 2.0 * (chassisRot.GetX() * chassisRot.GetX() + chassisRot.GetY() * chassisRot.GetY()); + + // Apply engine force + var force = new jolt.Jt.Vec3(forwardX * engineForce, forwardY * engineForce, forwardZ * engineForce); + physics.bodyInterface.AddForce(chassisBodyId, force); + #if hl force.delete(); #end + + // Apply steering torque + if (Math.abs(vehicleSteering) > 0.01 && currentSpeed > 0.5) { + var steerTorque = vehicleSteering * currentSpeed * 50; + var upTorque = new jolt.Jt.Vec3(0, 0, steerTorque); + physics.bodyInterface.AddTorque(chassisBodyId, upTorque); + #if hl upTorque.delete(); #end + } + + // Apply braking + if (brakingForce > 0 && currentSpeed > 0.1) { + var brakeVec = new jolt.Jt.Vec3(-vel.GetX() * brakingForce * 0.01, -vel.GetY() * brakingForce * 0.01, 0); + physics.bodyInterface.AddForce(chassisBodyId, brakeVec); + #if hl brakeVec.delete(); #end + } + + // Update object transform from physics + transform.loc.set(chassisPos.GetX(), chassisPos.GetY(), chassisPos.GetZ()); + transform.rot.set(chassisRot.GetX(), chassisRot.GetY(), chassisRot.GetZ(), chassisRot.GetW()); + transform.buildMatrix(); + + // Update wheel transforms with suspension simulation + for (i in 0...wheels.length) { + if (wheels[i] != null && i < wheelInfos.length) { + updateWheelTransform(i, chassisPos, chassisRot); + } + } + + // Free temporary native objects (after all usage) + #if hl + vel.delete(); + chassisRot.delete(); + chassisPos.delete(); + #end + } + + // Update camera + if (camera != null && camera.parent != null) { + camera.parent.transform.buildMatrix(); + } + if (camera != null) { + camera.buildMatrix(); + } + } + + function updateWheelTransform(wheelIndex:Int, chassisPos:jolt.Jt.RVec3, chassisRot:jolt.Jt.Quat) { + var wheel = wheels[wheelIndex]; + var info = wheelInfos[wheelIndex]; + + // Get wheel connection point in local space + var connPoint = info.getConnectionPoint(); + + // Simple suspension raycast + var worldX = chassisPos.GetX() + connPoint.x; + var worldY = chassisPos.GetY() + connPoint.y; + var worldZ = chassisPos.GetZ() - suspensionMaxLength; + + // Position wheel + wheel.transform.loc.set(worldX, worldY, worldZ + info.wheelRadius); + + // Rotate front wheels for steering + if (info.isFrontWheel) { + wheel.transform.rot.initRotateZ(vehicleSteering); + } + + wheel.transform.buildMatrix(); + } + + public function getSpeed():Float { + return currentSpeed; + } + + public function getSteeringAngle():Float { + return vehicleSteering; + } + + #if lnx_azerty + static inline var keyUp = "z"; + static inline var keyDown = "s"; + static inline var keyLeft = "q"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "a"; + #else + static inline var keyUp = "w"; + static inline var keyDown = "s"; + static inline var keyLeft = "a"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "q"; + #end +} + +class VehicleWheel { + public var isFrontWheel:Bool; + public var wheelRadius:Float; + public var wheelWidth:Float; + + var locX:Float; + var locY:Float; + var locZ:Float; + + public function new(id:Int, transform:Transform, vehicleTransform:Transform) { + wheelRadius = transform.dim.z / 2; + wheelWidth = transform.dim.x > transform.dim.y ? transform.dim.y : transform.dim.x; + + locX = transform.loc.x; + locY = transform.loc.y; + locZ = vehicleTransform.dim.z / 2 + transform.loc.z; + + isFrontWheel = id < 2; + } + + public function getConnectionPoint():Vec4 { + return new Vec4(locX, locY, locZ); + } +} + +#end