# https://xrfragment.org" # SPDX-License-Identifier: MPL-2.0" # author: Leon van Kammen # date: 16-05-2024 extends Node class_name XRF var scene: Node3D var URI: Dictionary = {} var history: Array var animplayer: AnimationPlayer var isModelLoading = false var metadata var _orphans = [] var _regex:RegEx = RegEx.new() var callback: Callable; var Type = { "isColor": "^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$", "isInt": "^[0-9]+$", "isFloat": "^[0-9]+%.[0-9]+$", "isVector": "([,]+|%w)" } # Called when the node enters the scene tree for the first time. func _ready(): pass # Replace with function body. # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): pass #################################################################################################### # URI Related functions # based on https://gist.github.com/coderofsalvation/b2b111a2631fbdc8e76d6cab3bea8f17 #################################################################################################### func parseURL(url: String) -> Dictionary: var URI = {"domain":"","fragment":"","file":"","URN":""} var parts = ["string","protocol","path","query","hash"] var urlregex = RegEx.new() urlregex.compile("(\\w+:\\/\\/)?([^#\\?]+)?(\\?[^#]+)?(#.*)?") var match = urlregex.search(url) for i in range(0,parts.size()): URI[ parts[i] ] = match.strings[i] if match.strings[i] else "" if URI["path"]: var pathParts:Array = URI["path"].split("/") if pathParts.size() > 1 and (pathParts[0].find(".") != -1 || pathParts[0].find(":") != -1): URI["domain"] = pathParts.pop_front() URI["path"] = "/".join(pathParts) pathParts = URI["path"].split("/") if pathParts[-1].find(".") != -1: URI["file"] = pathParts[-1] URI["path"] = "/".join(pathParts) URI["protocol"] = URI["protocol"].replace("://","") if URI["protocol"] else "" URI["fragment"] = parseArgs( URI["hash"].substr(1) ) if URI["hash"] else {} URI["query"] = parseArgs( URI["query"].substr(1) ) if URI["query"] else {} URI["URN"] = URI["string"].replace("\\?.*","") if URI["domain"] else "" URI["isLocal"] = true if !URI["domain"] else false # make relative URL's absolute if URI["isLocal"]: URI["domain"] = self.URI["domain"] URI["protocol"] = self.URI["protocol"] if URI["path"].match("\\/"): URI["path"] = self.URI["path"] + URI["path"] return URI func parseArgs(fragment: String) -> Dictionary: var ARG = {} var items = fragment.split("&") for item in items: var key_value = item.split("=") if key_value.size() > 1: ARG[key_value[0]] = guess_type(key_value[1]) else: ARG[key_value[0]] = "" return ARG func guess_type(str: String) -> Dictionary: var v = { "string": str, "x": null, "y": null, "color":null, "float": null, "int": null } var parts = str.split(",") if parts.size() > 1: v.x = parts[0].to_int() v.y = parts[1].to_int() if parts.size() > 2: v.z = parts[2].to_int() if str.match(Type.isColor): v.color = str if str.match(Type.isFloat): v.float = str.to_float() if str.match(Type.isInt): v.int = str.to_int() return v #################################################################################################### # Navigation Related functions #################################################################################################### func back(): var prev = self.history.pop_back() if prev != null: prev.next = self.URI self.to( prev.string, callback ) func forward(): if self.URI.next != null: self.to( self.URI.next.string, callback ) # Download model by HTTP and run `downloadModelSuccess` if OK func to(url, f:Callable ): print("navigating to "+url) cleanup() var URI = self.parseURL(url) callback = f if self.URI.has('domain') && URI.domain == self.URI.domain && URI.path == self.URI.path: URI.isLocal = true if !URI.isLocal: fetchURL(url, downloadModelSuccess ) if self.URI: self.URI.next = null self.history.push_back(self.URI ) self.URI = URI if URI.isLocal && URI.fragment.has('pos'): callback.call("teleport", self.posToTransform3D( URI.fragment.pos ) ) #################################################################################################### # Model Related functions #################################################################################################### func fetchURL(url:String, f:Callable) -> HTTPRequest: var http_request = HTTPRequest.new() _orphans.push_back(http_request) add_child(http_request) http_request.request_completed.connect(f) var error = http_request.request(url) if error != OK: print("could not request "+url) push_error("An error occurred in the HTTP request.") return http_request func cleanup(): for orphan in _orphans: remove_child(orphan) func downloadModelSuccess(result, response_code, headers, body): # TODO: here different parsing functions should be called # based on the filetype (glb,gltf,ade,obj e.g.) loadModelFromBufferByGLTFDocument(body) if scene == null: print('could not load GLTF from HTTP response') return _parseXRFMetadata(scene) traverse( scene, _parseXRFMetadata ) # setup actions & embeds traverse( scene, href.init ) traverse( scene, src.init ) setPredefinedSceneView() callback.call("scene_loaded", scene) func loadModelFromBufferByGLTFDocument(body): var doc = GLTFDocument.new() var state = GLTFState.new() #state.set_handle_binary_image(GLTFState.HANDLE_BINARY_EMBED_AS_BASISU) # Fixed in new Godot version (4.3 as I see) https://github.com/godotengine/godot/blob/17e7f85c06366b427e5068c5b3e2940e27ff5f1d/scene/resources/portable_compressed_texture.cpp#L116 var error = doc.append_from_buffer(body, "", state, 8) # 8 = force ENABLE_TANGENTS since it is required for mesh compression since 4.2 if error == OK: scene = doc.generate_scene(state) scene.name = "XRFscene" metadata = _parseMetadata(state,scene) add_child(scene) print("model added") _addAnimations(state, scene) else: print("Couldn't load glTF scene (error code: %s). Are you connected to internet?" % error_string(error)) func _parseXRFMetadata(node:Node): if node.has_meta("extras"): var extras = node.get_meta("extras") var XRF = {} for i in extras: if typeof(extras[i]) == TYPE_STRING: XRF[ i ] = parseURL( extras[i] ) node.set_meta("XRF", XRF) func _addAnimations( state:GLTFState, scene:Node): self.animplayer == null for i in scene.get_child_count(): var animplayer : AnimationPlayer = scene.get_child(i) as AnimationPlayer; if animplayer == null: continue; self.animplayer = animplayer print("playing animations") print(animplayer.get_animation_library_list()) var anims = animplayer.get_animation_library_list() for j in anims: animplayer.play( j ) func traverse(node, f:Callable ): for N in node.get_children(): if N.get_child_count() > 0: f.call(N) self.traverse(N,f) else: f.call(N) func _parseMetadata(state: GLTFState, scene: Node) -> Error: #var meta = new Dictionary() # Add metadata to materials var materials_json : Array = state.json.get("materials", []) var materials : Array[Material] = state.get_materials() for i in materials_json.size(): if materials_json[i].has("extras"): materials[i].set_meta("extras", materials_json[i]["extras"]) # Add metadata to ImporterMeshes var meshes_json : Array = state.json.get("meshes", []) var meshes : Array[GLTFMesh] = state.get_meshes() for i in meshes_json.size(): if meshes_json[i].has("extras"): meshes[i].mesh.set_meta("extras", meshes_json[i]["extras"]) # Add metadata to scene var scenes_json : Array = state.json.get("scenes", []) if scenes_json[0].has("extras"): scene.set_meta("extras", scenes_json[0]["extras"]) # Add metadata to nodes var nodes_json : Array = state.json.get("nodes", []) for i in nodes_json.size(): if nodes_json[i].has("extras"): var name = nodes_json[i]["name"].replace(".","_") var node = scene.find_child(name) #state.get_scene_node(i) if node: node.set_meta( "extras", nodes_json[i]["extras"] ) else: print(name+" could not be found") return OK func posToTransform3D(v:Dictionary): var transform : Transform3D if !v.x: var node:Node3D = scene.find_child(v.string) if node: transform = node.global_transform else: var pos = Vector3() pos.x = v.x pos.y = v.y pos.z = v.z transform = Transform3D() transform.origin = pos return transform #################################################################################################### # The XR Fragments # spec: https://xrfragment.org/doc/RFC_XR_Fragments.html #################################################################################################### # info: https://xrfragment.org/#predefined_view # spec: 6-8 @ https://xrfragment.org/doc/RFC_XR_Fragments.html#navigating-3d func setPredefinedSceneView(): var XRF = scene.get_meta("XRF") if XRF && XRF.has("#") && XRF["#"]["fragment"]["pos"]: self.URI.fragment = XRF["#"]["fragment"] if !self.URI.string.match("#"): self.URI.string += XRF["#"]["string"] callback.call("teleport", posToTransform3D(XRF["#"]["fragment"]["pos"]) ) func href_init(node:Node): if node.has_meta("XRF"): var XRF = node.get_meta("XRF") if XRF.has('href'): var parent = node.get_parent() var area3D = Area3D.new() var col3D = CollisionShape3D.new() var group = MeshInstance3D.new() parent.remove_child(node) area3D.add_child(node) area3D.add_child(col3D) col3D.make_convex_from_siblings() # generate collision from MeshInstance3D siblings parent.add_child(area3D) var href = { "click": func init(node:Node): if node.has_meta("XRF"): var XRF = node.get_meta("XRF") if XRF.has('href'): print("TELEPORT") to(XRF.href.string,callback) callback.call("href", node), "init": func href_init(node:Node): if node.has_meta("XRF"): var XRF = node.get_meta("XRF") if XRF.has('href'): var parent = node.get_parent() var area3D = Area3D.new() var col3D = CollisionShape3D.new() var group = MeshInstance3D.new() parent.remove_child(node) area3D.add_child(node) area3D.add_child(col3D) col3D.make_convex_from_siblings() # generate collision from MeshInstance3D siblings parent.add_child(area3D) } var src = { "extension":{}, "addExtension": func addExtension( extension:String, f:Callable): # flexible way for adding extension handlers src.extension[ extension ] = f, "init": func init(node:Node): if node.has_meta("XRF"): var XRF = node.get_meta("XRF") if XRF.has('src'): var mesh = node as MeshInstance3D if mesh != null: var mat = mesh.get_active_material(0) as BaseMaterial3D mat = mat.duplicate() mat.transparency = mat.TRANSPARENCY_ALPHA mat.albedo = Color(1.0,1.0,1.0, 0.3) # 0.5 sets 50% opacity mesh.set_surface_override_material(0,mat) for ext in src.extension: _regex.compile("^.*."+ext+"$") if _regex.search(XRF.src.path): var url:String = XRF.src.protocol+"://"+XRF.src.domain+XRF.src.path print("src: fetching "+url) var handler:Callable = src.extension[ext].call(node,ext) fetchURL(url, handler ) callback.call("src", {"node":node,"XRF":XRF} ), # some builtin handlers "audio": func audio(node:Node, extension:String) -> Callable: return func onFile(result, response_code, headers, body): var src = node.get_meta("XRF").src var music = AudioStreamPlayer.new() add_child(music) var audio_loader = AudioLoader.new() music.set_stream( audio_loader.loadfile( src.file, body ) ) music.volume_db = 1 music.pitch_scale = 1 music.play() add_child(music) }