godotengine / godot-docs

Godot Engine official documentation
https://docs.godotengine.org
Other
3.95k stars 3.22k forks source link

godot 4 glTF runtime loader #6385

Open flobotics opened 2 years ago

flobotics commented 2 years ago

hi, i searched alot, but can only find old github post about it. Is there a doc about the new glTF runtime loader in godot 4 ?

Is it possible to e.g. start a multiplayer-networking-game and while playing, another persons sends per e.g. email a new glTF (e.g. mesh+texture) and you want to load it into the running multiplayer-networking-game without rebuilding/reloading/redistribution ?

thx

Calinou commented 2 years ago

cc @fire

fire commented 2 years ago

I don't have a detailed answer, but here's a few minutes paragraph.

The feature is using GLTFDocument and GLTFExtensions to load glb etc. at runtime like from a buffer.

You can do it, but it's not fully optimized for runtime usage.

Will see if I can get some examples, but I'm a bit slow.

flobotics commented 2 years ago

ok, i will do some tests

fire commented 2 years ago
  1. Supply a glb in a byte buffer.
  2. GLTFDocument has an append_from_buffer, which will give you a GLTFState.
  3. You can then pass the gltf state to generate scene.
flobotics commented 2 years ago

thx, i am new to gdscript.

I use the Fox.glb to have a good base ( https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox/glTF-Binary )

  1. supply a glb in a byte buffer var myfox = load("res://Fox.glb") #loading should be a normal filesystem path var myArr = PackedByteArray() ??? how put the myfox in myArr ???

  2. GLTFDocument has an append_from_buffer, which will give you a GLTFState. What is the second parameter "base_path" of append_from_buffer() ??

need to learn more GDScript

flobotics commented 2 years ago

i can load the fox with

var gltf := GLTFDocument.new() var gltf_state := GLTFState.new()

var path = "C:/Users/SuperUserName/Documents/godot-workspace/godot-data/Fox.glb"
var snd_file = FileAccess.open(path, FileAccess.READ)

var fileBytes = PackedByteArray()
fileBytes = snd_file.get_buffer(snd_file.get_length())

gltf.append_from_buffer(fileBytes, "base_path?", gltf_state)

var node = gltf.generate_scene(gltf_state)
add_child(node)

I use "base_path?" as parameter and it seemed to work ?

Now testing with multiplayer networking

fire commented 1 year ago

For a byte buffer setting base path to "" (empty) string is fine. It's a feature to not require access to the filesystem.

fire commented 1 year ago

Working on the docs https://github.com/godotengine/godot/pull/68933

flobotics commented 1 year ago

thanks for the base_path info.

still trying/working on a multiplayer network test for glTF runtime loader cross platform (i am new to godot :) )

flobotics commented 1 year ago

hi, i try to spawn the Fox.glb file from a networking server and it should be displayed also on the clients.

I use https://github.com/TheGodojo/Godot-4-Networking-Demonstration-Complete as a simple test project.

In the player_character.gd script i added:

@rpc(any_peer, call_local, reliable, 1)
func spawnGlTF():
    # if we are the server (could be multiplayer.is_server() ? )
    if multiplayer.get_unique_id() == 1:
        print("spawnGlTF")
        var gltf := GLTFDocument.new()
        var gltf_state := GLTFState.new()

        var path = "C:/Users/SuperUserName/Documents/godot-workspace/godot-data/Fox.glb"
        if FileAccess.file_exists(path):
            print("File to spawn exists")
        else:
            print("File to spawn does not exist")
            return

        var snd_file = FileAccess.open(path, FileAccess.READ)

        var fileBytes = PackedByteArray()
        fileBytes = snd_file.get_buffer(snd_file.get_length())

        gltf.append_from_buffer(fileBytes, "base_path?", gltf_state)

        var node = gltf.generate_scene(gltf_state)
        add_child(node) 
    else:
        print("notSpawnGlTF")

and call it simply when someone clicks on the player_character

func _on_mouse_click_area_input_event(camera, event, position, normal, shape_idx):
    if event is InputEventMouseButton:
        rpc("clicked_by_player")
        rpc("spawnGlTF")
fox-load

The fox is loaded as a child of the player-1, but only on the server side ? What do i wrong/miss ?

thx

fire commented 1 year ago

I think this is a networking problem @Faless probably knows immediately. I think it's because the scene paths don't match.

fire commented 1 year ago

I've been also playing with a runtime loader.

https://github.com/V-Sekai-fire/VSK_model_explorer image

image

Reference

https://github.com/V-Sekai-fire/VSK_model_explorer/blob/b5d453cd1e193c4a5a3a2cff786a9c60afaf6204/scene/ModelExplorer.gd#L32-L52

flobotics commented 1 year ago

hi @fire i tried your model exporter, but its not working. I made issue https://github.com/V-Sekai-fire/VSK_model_explorer/issues/1

fire commented 1 year ago

I may be using a newer version of the engine than is released.

image

Trying to get a release_template built but having trouble.

flobotics commented 1 year ago

i followed your hint @fire , tried setting some path on GLDocument and GLState, but nothing changed. I also had a look at multiplayersynchronizer but that seemed to update properties only.

Perhaps @Faless could you give me some hints, where to look at ?

thx

flobotics commented 1 year ago

When i set the multiplayer_authority and then request if i set it, it returns false ? But i think i use multiplayerSpawner wrong, perhaps someone got a hint ?

@rpc(any_peer, call_local, reliable, 1)
func spawnGlTF():
    # if we are the server (could be multiplayer.is_server() ? )
    if multiplayer.get_unique_id() == 1:
        print("spawnGlTF")
        var gltf := GLTFDocument.new()
        var gltf_state := GLTFState.new()

        var path = "C:/Users/SuperUserName/Documents/godot-workspace/godot-data/Fox.glb"
        if FileAccess.file_exists(path):
            print("File to spawn exists")
        else:
            print("File to spawn does not exist")
            return

        var snd_file = FileAccess.open(path, FileAccess.READ)

        var fileBytes = PackedByteArray()
        fileBytes = snd_file.get_buffer(snd_file.get_length())

        gltf.append_from_buffer(fileBytes, "base_path?", gltf_state)

        var node = gltf.generate_scene(gltf_state)
        #this adds fox the the player as child, so it moves with the player

#       E 0:00:13:0789   spawnGlTF: Cannot get path of node as it is not in a scene tree.
#         <C++-Fehler>   Condition "!is_inside_tree()" is true. Returning: NodePath()
#         <C++-Quellcode>scene/main/node.cpp:1715 @ get_path()
#         <Stacktrace>   player_character.gd:66 @ spawnGlTF()
        add_child(node) 

        var sceneTree = get_tree().root
#       sceneTree.add_child(node)

        #multiplayersyncronizer test
        var gltf_spawner := MultiplayerSpawner.new()
        print("scene count:" +str(gltf_spawner.get_spawnable_scene_count()) )
        gltf_spawner.add_spawnable_scene(node.get_path())
        print("scene count2:" + str(gltf_spawner.get_spawnable_scene_count()) )
#       gltf_spawner.spawn_path = "."
        gltf_spawner.set_multiplayer_authority(get_multiplayer_authority())
        if gltf_spawner.is_multiplayer_authority():
            print("is multiplayer authority")
        else:
            print("is not multiplayer authority")

        if gltf_spawner.is_inside_tree():
            print("is inside tree")
        else:
            print("is not inside tree")

        if multiplayer.has_multiplayer_peer():
            print("has multiplayer peer")
        else:
            print("has not multiplayer peer")

        gltf_spawner.spawn()

        #some tree tests
#       gltf_state.scene_name = "Main"
#       gltf_state.resource_path = "res://Fox.glb"
#       gltf_state.resource_name = "Fox.glb"
#       print("rsc name " + gltf_state.resource_name )
#       print("rsc path " + gltf_state.resource_path )
#       print("nooode " + gltf_state.scene_name )
#       var sceneTree = get_tree().root
#       sceneTree.add_child(node)

    else:
        print("notSpawnGlTF")
spawn1 spawn2
flobotics commented 1 year ago

i tried packing the Fox.glb into a scene, save it with res:// path and then load it again, so that it has a res:// path for add_spawnable_scene() function, but it wont work ( !is_inside_tree ) . The function Node::_propagate_enter_tree() is setting is_inside_tree to true, but dont know yet how to get there?

void Node::_propagate_enter_tree() {
    // this needs to happen to all children before any enter_tree

    if (data.parent) {
        data.tree = data.parent->data.tree;
        data.depth = data.parent->data.depth + 1;
    } else {
        data.depth = 1;
    }

    data.viewport = Object::cast_to<Viewport>(this);
    if (!data.viewport && data.parent) {
        data.viewport = data.parent->data.viewport;
    }

    data.inside_tree = true;

My try with saving and loading glb as scene :)

@rpc(any_peer, call_local, reliable, 1)
func spawnGlTF():
    # if we are the server (could be multiplayer.is_server() ? )
    if multiplayer.get_unique_id() == 1:
        print("spawnGlTF")
        var gltf := GLTFDocument.new()
        var gltf_state := GLTFState.new()

        var path = "C:/Users/SuperUserName/Documents/godot-workspace/godot-data/Fox.glb"
        if FileAccess.file_exists(path):
            print("File to spawn exists")
        else:
            print("File to spawn does not exist")
            return

        var snd_file = FileAccess.open(path, FileAccess.READ)

        var fileBytes = PackedByteArray()
        fileBytes = snd_file.get_buffer(snd_file.get_length())

        gltf.append_from_buffer(fileBytes, "base_path?", gltf_state)

        var node = gltf.generate_scene(gltf_state)

        # save gltf as a scene with res:// path, to later add the scene to MultiplayerSpawner.add_spawnable_scene ??
        var scene = PackedScene.new()
        var result = scene.pack(node)
        if result == OK:
            var error = ResourceSaver.save(scene, "res://myscene.scn")  # Or "user://..."
            if error != OK:
                print("An error occurred while saving the scene to disk.")

        # load the scene from res:// path
        var loadscene = ResourceLoader.load("res://myscene.scn")

        # add node to tree for is_inside_tree ??
        add_child(loadscene.get_local_scene())
#       E 0:00:14:0633   spawnGlTF: Parameter "p_child" is null.
#         <C++-Quellcode>scene/main/node.cpp:1127 @ add_child()
#         <Stacktrace>   player_character.gd:70 @ spawnGlTF()

        #multiplayerspawner test
        var gltf_spawner := MultiplayerSpawner.new()
        print("gltf_spawner.spawn_path:" + str(gltf_spawner.spawn_path) )
        gltf_spawner.spawn_path = "." # "/root/" # node.get_path()
        print("gltf_spawner.spawn_path:" + str(gltf_spawner.spawn_path) )

        print("scene count:" +str(gltf_spawner.get_spawnable_scene_count()) )

#       print("node-get_path:" + str(node.get_path()) )
        gltf_spawner.add_spawnable_scene(loadscene.resource_path)

        print("scene count2:" + str(gltf_spawner.get_spawnable_scene_count()) )
#       gltf_spawner.spawn_path = "."
        gltf_spawner.set_multiplayer_authority(multiplayer.get_unique_id() ) # get_multiplayer_authority())
        if gltf_spawner.is_multiplayer_authority():
            print("is multiplayer authority")
        else:
            print("is not multiplayer authority")

        if gltf_spawner.is_inside_tree():
            print("is inside tree")
        else:
            print("is not inside tree")

        if multiplayer.has_multiplayer_peer():
            print("has multiplayer peer")
        else:
            print("has not multiplayer peer")

#       gltf_spawner.spawn()

I dont need to copy the scene to all multiplayer clients with RPC , or? that copying is the spawner doing, or ?

thx

flobotics commented 1 year ago

seemed i got it running with godot 4 beta 8.

If interrested , my first working code, to reproduce i use https://github.com/TheGodojo/Godot-4-Networking-Demonstration-Complete and change

func _on_host_pressed():
    $NetworkInfo/NetworkSideDisplay.text = "Server"
    $Menu.visible = false
    multiplayer_peer.create_server(PORT)
    multiplayer.multiplayer_peer = multiplayer_peer
    $NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())

    add_player_character(1)

    multiplayer_peer.peer_connected.connect(
        func(new_peer_id):
            await get_tree().create_timer(1).timeout
            rpc("add_newly_connected_player_character", new_peer_id)
            rpc_id(new_peer_id, "add_previously_connected_player_characters", connected_peer_ids)
            add_player_character(new_peer_id)
            spawnGlTF()
    )

@rpc(any_peer, call_local, reliable, 1)
func spawnGlTF():
    # if we are the server (could be multiplayer.is_server() ? )
    if multiplayer.get_unique_id() == 1:
        print("spawnGlTF")

        var gltf := GLTFDocument.new()
        var gltf_state := GLTFState.new()

        var path = "C:/Users/SuperUserName/Documents/godot-workspace/godot-data/Fox.glb"
        if FileAccess.file_exists(path):
            print("File to spawn exists")
        else:
            print("File to spawn does not exist")
            return

        var snd_file = FileAccess.open(path, FileAccess.READ)

        var fileBytes = PackedByteArray()
        fileBytes = snd_file.get_buffer(snd_file.get_length())

        gltf.append_from_buffer(fileBytes, "base_path?", gltf_state)

        var node = gltf.generate_scene(gltf_state)

        rpc("send_gltf_scene_to_peers", fileBytes)

        add_child(node)

    else:
        print("notSpawnGlTF")

@rpc(any_peer, call_local, reliable, 1)
func send_gltf_scene_to_peers(fileBytes):
    print("send-gltf-scene-to-peers")

    if multiplayer.get_unique_id() != 1:
        var gltf := GLTFDocument.new()
        var gltf_state := GLTFState.new()
        gltf.append_from_buffer(fileBytes, "base_path?", gltf_state)

        var node = gltf.generate_scene(gltf_state)
        add_child(node)
        print("We are NOT server, add_child()")
    else:
        print("We are server, doing nothing")
    pass
spawn3
flobotics commented 1 year ago

wrote a "distributor" , where you can request a not-at-compile-time-known-glb, the glb gets distributed across the clients and added to add_spawnable_scene(). Then you can use it with "normal" MultiplayerSpawner capabilities, I mean, you can add_child() it, and all clients also automatically load it too. add_child() only from server at present time possible.

first minimal-only-windows try https://github.com/flobotics/godot/tree/glb_distributor

glb_distributor

I think i handle many things wrong , so any help or hint is welcome

thx

mororo250 commented 1 year ago

Hi everyone, We have the same problem in my company, and this is the way we are doing it here: It might be a good reference on how to do it using c#. However, we are still trying to figure it out. If it's possible to do remapping as well at runtime, it seems impossible.

To load a typical 3d scene:

       private GltfDocument GltfImporter;

       GltfState gltfState = new();
        GltfImporter.AppendFromFile(filepath, gltfState);
        AgentModels[index] = GltfImporter.GenerateScene(gltfState) as Node3D;

To load an animations library:

        GltfState gltfState = new();
        GltfImporter.AppendFromFile(filepath, gltfState);
        Node3D scene =  GltfImporter.GenerateScene(gltfState) as Node3D;

        AnimationLibrary animations = null;
        for (int i = 0; i < scene.GetChildCount(); i++)
        {
            AnimationPlayer? animationPlayer = scene.GetChild(i) as AnimationPlayer;
            if (animation player == null)
                continue;

            Array<StringName> libs = animationPlayer.GetAnimationLibraryList();
            if (libs. Count >= 0)
            {
                animations = animationPlayer.GetAnimationLibrary(libs[0]);
                // Set animation loop to true -> Because of the way our animation system works its better to have all of the animations imported as a loop.
                foreach (string animationName in animations.GetAnimationList())
                {
                    animations.GetAnimation(animationName).LoopMode = Animation.LoopModeEnum.Linear;
                }
                break;
            }
mason-stafford commented 1 year ago

is there any way to background load glb resources at runtime while resource file is not in project directory?