Next patch

This commit is contained in:
2026-02-24 21:30:00 -08:00
parent d45c632dcd
commit cd3090817a
33 changed files with 4490 additions and 398 deletions

View File

@ -44,6 +44,7 @@ typedef TConfig = {
@:optional var rp_supersample: Null<Float>;
@:optional var rp_shadowmap_cube: Null<Int>; // size
@:optional var rp_shadowmap_cascade: Null<Int>; // size for single cascade
@:optional var rp_ssao: Null<Bool>;
@:optional var rp_ssgi: Null<Bool>;
@:optional var rp_ssr: Null<Bool>;
@:optional var rp_ssrefr: Null<Bool>;

View File

@ -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<String, WebSocketServer<HostHandler>> = [];
#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<HostHandler>(net_Domain, net_Port, net_Max);
#if (sys || kha_krom)
if (connections[net_Url] == null) {
connections[net_Url] = new WebSocketServer<HostHandler>(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<String, WebSocketSecureServer<SecureHostHandler>> = [];
#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<SecureHostHandler>(net_Domain, net_Port, cert, key, cert, net_Max);
if (connections[net_Url] == null) {
connections[net_Url] = new WebSocketSecureServer<SecureHostHandler>(net_Domain, net_Port, cert, key, cert, net_Max);
}
#elseif kha_krom
if (connections[net_Url] == null) {
connections[net_Url] = new WebSocketSecureServer<SecureHostHandler>(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

View File

@ -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<String, leenkx.network.LeenkxSocket> = [];
#if js
public static var peers:js.lib.Map<String, String> = new js.lib.Map<String,String>();
public static var data:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
public static var id:js.lib.Map<String, String> = new js.lib.Map<String,String>();
public static var torrent:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
public static var file:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
public static var file:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
#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
}
}
}

View File

@ -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

View File

@ -1,3 +1,7 @@
package leenkx.network;
#if kha_krom
typedef SecureSocketImpl = leenkx.network.krom.KromSecureSocket;
#else
typedef SecureSocketImpl = sys.ssl.Socket;
#end

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<T> {
#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;

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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<KromSocket> = [];
private var _newConnections:Array<KromSocket> = [];
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<KromSocket>, write:Array<KromSocket>, others:Array<KromSocket>, ?timeout:Float):{read:Array<KromSocket>, write:Array<KromSocket>, others:Array<KromSocket>} {
var readable:Array<KromSocket> = [];
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;
}
}

View File

@ -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<Mat4>, effector: TObj, goal: Vec4, precision = 0.01, maxIterations = 100, chainLenght = 100, pole: Vec4 = null, rollAngle = 0.0, influence = 0.0, layerMask: Null<Int> = 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<Mat4>, effector: TObj, goal: Vec4, pole: Vec4 = null, rollAngle = 0.0, influence = 0.0, layerMask: Null<Int> = 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<Vec2>, sampleVec: Vec2): Map<Int, Vec2> {
var weightIndex: Array<WeightIndex> = [];
@ -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<Int>;
@ -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;

View File

@ -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
}
}

View File

@ -14,6 +14,13 @@ class Inc {
static var spotIndex = 0;
static var lastFrame = -1;
#if lnx_shadowmap_atlas
static var tilesToRemove: Array<ShadowMapTile> = [];
#if lnx_shadowmap_atlas_lod
static var tilesToChangeSize: Array<ShadowMapTile> = [];
#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<ShadowMapTile> = [];
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;
}

View File

@ -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
}

View File

@ -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 = "";

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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<LineData> = [];
final texts:Array<TextData> = [];
var font:kha.Font = null;
var rayCasts:Array<TRayCastData> = [];
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

View File

@ -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

View File

@ -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<Float>;
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<Float> = 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

View File

@ -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<Float>;
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<Float>) {
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

View File

@ -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<ContactPair>;
var preUpdates:Array<Void->Void> = null;
public var rbMap:Map<Int, RigidBody>;
public var conMap:Map<Int, PhysicsConstraint>;
public var constraints:Array<PhysicsConstraint> = [];
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<PhysicsWorld> = [];
#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<RigidBody> {
if (contacts.length == 0)
return null;
var res:Array<RigidBody> = [];
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<ContactPair> {
if (contacts.length == 0)
return null;
var res:Array<ContactPair> = [];
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

View File

@ -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:Array<RigidBody->Void> = 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

View File

@ -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<SoftParticle> = [];
var springs:Array<SoftSpring> = [];
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<Int, Array<Int>>;
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<String, Bool> = 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<String, Bool> = 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<String, Bool>) {
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

View File

@ -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<Object> = [];
var wheelInfos:Array<VehicleWheel> = [];
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