diff --git a/__main__.py b/__main__.py index bbedd24..4892972 100644 --- a/__main__.py +++ b/__main__.py @@ -1,9 +1,15 @@ import argparse -from source.game import start_game +from source.pkgexplore import start_explorer + +def main(): + parser = argparse.ArgumentParser(prog='MBH', description='Morrowind with Blackjack and Hookers', epilog='?') + parser.add_argument('-m', '--main', required=True) + parser.add_argument('-p', '--package', nargs='+') + parser.add_argument('-e', '--explore', action='store_true') + args = parser.parse_args() + if args.explore: + start_explorer(args) if __name__ == '__main__': - parser = argparse.ArgumentParser(prog='MBH', description='Morrowind with Blackjack and hookers', epilog='?') - parser.add_argument('-p', '--package', nargs='+') - args = parser.parse_args() - start_game(args.package) + main() \ No newline at end of file diff --git a/assets/gamepackage.json b/assets/gamepackage.json index df69e31..7a533c3 100644 --- a/assets/gamepackage.json +++ b/assets/gamepackage.json @@ -1,4 +1,5 @@ { "name": "main-assets", - "version": "0.1-alpha" + "version": "0.1-alpha", + "type": "main" } \ No newline at end of file diff --git a/assets/models/armatures/normalhuman.glb b/assets/models/armatures/normalhuman.glb new file mode 100644 index 0000000..bab3082 Binary files /dev/null and b/assets/models/armatures/normalhuman.glb differ diff --git a/assets/models/armatures/playerviewarmature.glb b/assets/models/armatures/playerviewarmature.glb new file mode 100644 index 0000000..903e731 Binary files /dev/null and b/assets/models/armatures/playerviewarmature.glb differ diff --git a/assets/models/armatures/playerviewarmature2.glb b/assets/models/armatures/playerviewarmature2.glb new file mode 100644 index 0000000..ca6f256 Binary files /dev/null and b/assets/models/armatures/playerviewarmature2.glb differ diff --git a/assets/models/arrangements/arrange1.glb b/assets/models/arrangements/arrange1.glb index db60b6b..7847855 100644 Binary files a/assets/models/arrangements/arrange1.glb and b/assets/models/arrangements/arrange1.glb differ diff --git a/assets/models/arrangements/arrange2.glb b/assets/models/arrangements/arrange2.glb new file mode 100644 index 0000000..dedb22e Binary files /dev/null and b/assets/models/arrangements/arrange2.glb differ diff --git a/assets/models/arrangements/arrange3.glb b/assets/models/arrangements/arrange3.glb new file mode 100644 index 0000000..dedb22e Binary files /dev/null and b/assets/models/arrangements/arrange3.glb differ diff --git a/assets/models/arrangements/arrange4.glb b/assets/models/arrangements/arrange4.glb new file mode 100644 index 0000000..5dab7e1 Binary files /dev/null and b/assets/models/arrangements/arrange4.glb differ diff --git a/assets/models/arrangements/test/arrange1.glb b/assets/models/arrangements/test/arrange1.glb new file mode 100644 index 0000000..7847855 Binary files /dev/null and b/assets/models/arrangements/test/arrange1.glb differ diff --git a/assets/models/axis.glb b/assets/models/axis.glb new file mode 100644 index 0000000..d6e472b Binary files /dev/null and b/assets/models/axis.glb differ diff --git a/assets/models/box.glb b/assets/models/box.glb new file mode 100644 index 0000000..55dd1dd Binary files /dev/null and b/assets/models/box.glb differ diff --git a/assets/models/normalhuman_mesh.glb b/assets/models/normalhuman_mesh.glb new file mode 100644 index 0000000..d06caed Binary files /dev/null and b/assets/models/normalhuman_mesh.glb differ diff --git a/assets/models/notamodel.txt b/assets/models/notamodel.txt new file mode 100644 index 0000000..e69de29 diff --git a/assets/models/suz.glb b/assets/models/suz.glb new file mode 100644 index 0000000..5bf52af Binary files /dev/null and b/assets/models/suz.glb differ diff --git a/assets/models/sword1/sword1.glb b/assets/models/sword1/sword1.glb new file mode 100644 index 0000000..f8932f7 Binary files /dev/null and b/assets/models/sword1/sword1.glb differ diff --git a/assets/models/sword1/sword2.glb b/assets/models/sword1/sword2.glb new file mode 100644 index 0000000..c40eee2 Binary files /dev/null and b/assets/models/sword1/sword2.glb differ diff --git a/assets/models/sword1/sword3.glb b/assets/models/sword1/sword3.glb new file mode 100644 index 0000000..a08fa8a Binary files /dev/null and b/assets/models/sword1/sword3.glb differ diff --git a/assets/textures/axis.png b/assets/textures/axis.png new file mode 100644 index 0000000..fcee5f5 Binary files /dev/null and b/assets/textures/axis.png differ diff --git a/assets/textures/cursed.png b/assets/textures/cursed.png new file mode 100644 index 0000000..7f94744 Binary files /dev/null and b/assets/textures/cursed.png differ diff --git a/assets/textures/sword1/albedo.png b/assets/textures/sword1/albedo.png new file mode 100644 index 0000000..3a520cb Binary files /dev/null and b/assets/textures/sword1/albedo.png differ diff --git a/assets/textures/sword1/ao.png b/assets/textures/sword1/ao.png new file mode 100644 index 0000000..17131c7 Binary files /dev/null and b/assets/textures/sword1/ao.png differ diff --git a/assets/textures/sword1/normal.png b/assets/textures/sword1/normal.png new file mode 100644 index 0000000..b0bf264 Binary files /dev/null and b/assets/textures/sword1/normal.png differ diff --git a/assets/textures/sword1/roughness.png b/assets/textures/sword1/roughness.png new file mode 100644 index 0000000..3947a09 Binary files /dev/null and b/assets/textures/sword1/roughness.png differ diff --git a/icons/application.png b/icons/application.png new file mode 100644 index 0000000..1dee9e3 Binary files /dev/null and b/icons/application.png differ diff --git a/icons/brick.png b/icons/brick.png new file mode 100644 index 0000000..97a152c Binary files /dev/null and b/icons/brick.png differ diff --git a/icons/bricks.png b/icons/bricks.png new file mode 100644 index 0000000..0905f93 Binary files /dev/null and b/icons/bricks.png differ diff --git a/icons/color_swatch.png b/icons/color_swatch.png new file mode 100644 index 0000000..5597faf Binary files /dev/null and b/icons/color_swatch.png differ diff --git a/icons/color_wheel.png b/icons/color_wheel.png new file mode 100644 index 0000000..809fb00 Binary files /dev/null and b/icons/color_wheel.png differ diff --git a/icons/computer.png b/icons/computer.png new file mode 100644 index 0000000..9bc37dc Binary files /dev/null and b/icons/computer.png differ diff --git a/icons/folder.png b/icons/folder.png new file mode 100644 index 0000000..784e8fa Binary files /dev/null and b/icons/folder.png differ diff --git a/icons/page_red.png b/icons/page_red.png new file mode 100644 index 0000000..0b18247 Binary files /dev/null and b/icons/page_red.png differ diff --git a/icons/picture.png b/icons/picture.png new file mode 100644 index 0000000..4a158fe Binary files /dev/null and b/icons/picture.png differ diff --git a/icons/pictures.png b/icons/pictures.png new file mode 100644 index 0000000..d9591c1 Binary files /dev/null and b/icons/pictures.png differ diff --git a/icons/shape_group.png b/icons/shape_group.png new file mode 100644 index 0000000..bb2ff51 Binary files /dev/null and b/icons/shape_group.png differ diff --git a/icons/shape_square.png b/icons/shape_square.png new file mode 100644 index 0000000..33af046 Binary files /dev/null and b/icons/shape_square.png differ diff --git a/source/filesystem.py b/source/filesystem.py deleted file mode 100644 index a6dd50d..0000000 --- a/source/filesystem.py +++ /dev/null @@ -1,100 +0,0 @@ - -import os -import posixpath -import json -from panda3d.core import VirtualFileSystem - -class PackageNotFound(FileNotFoundError): - pass - -class PackageMetaNotFound(FileNotFoundError): - pass - -class PackageInvalid(FileNotFoundError): - pass - -class PackageDuplicate(FileNotFoundError): - pass - -class Package: - def __init__(self, name, vfs_dir): - self.name = name - self.vfs_dir = vfs_dir - -class Filesystem: - def __init__(self): - self.package_list = [] - self.package_map = {} - self.vfs = VirtualFileSystem.getGlobalPtr() - self.include('assets/') - self.mdl_missing = loader.loadModel('/main-assets/models/missing.glb') - self.mdl_missing.setTexture(loader.loadTexture('/main-assets/textures/missing_mdl.png')) - self.tex_missing = loader.loadTexture('/main-assets/textures/missing_tex.png') - - def include(self, path): - if not os.path.exists(path): - raise PackageNotFound(path) - gamepkg_path = os.path.join(path, 'gamepackage.json') - if not os.path.exists(gamepkg_path): - raise PackageMetaNotFound(path) - meta = json.load(open(gamepkg_path)) - if 'name' not in meta: - raise PackageInvalid(path) - if meta['name'] in self.package_map: - raise PackageDuplicate(meta['name']) - vfs_path = f'/{meta["name"]}' - vfs.mount(path, f'/{meta["name"]}', 0) - new_package = Package(meta["name"], vfs_path) - self.package_map[meta['name']] = new_package - self.package_list.insert(0, new_package) - - def find_texture(self, tex_path): - try: - if tex_path[0:2] == '$/': - for package in self.package_list: - path = posixpath.join('/', package.name, 'textures', tex_path[2:]) - if self.vfs.exists(path): - return loader.loadTexture(path) - return self.tex_missing - else: - resource_path = model_path.split('/') - path = posixpath.join('/', resource_path[0], 'textures', '/'.join(resource_path[1:])) - return loader.loadTexture(path) - except IOError: - return self.tex_missing - - def find_model(self, model_path): - try: - if model_path[0:2] == '$/': - for package in self.package_list: - path = posixpath.join('/', package.name, 'models', model_path[2:]) - if self.vfs.exists(path): - return loader.loadModel(path) - return self.mdl_missing - else: - resource_path = model_path.split('/') - path = posixpath.join('/', resource_path[0], 'models', '/'.join(resource_path[1:])) - return loader.loadModel(path) - except IOError: - return self.mdl_missing - - - - - - - - - - - - - - - - - - - - - diff --git a/source/game.py b/source/game.py index c82550d..78db288 100644 --- a/source/game.py +++ b/source/game.py @@ -1,30 +1,24 @@ -from .filesystem import Filesystem +from .packages import PackageManager from direct.showbase.ShowBase import ShowBase -from panda3d.core import WindowProperties +from direct.actor.Actor import Actor +from panda3d.core import WindowProperties, TextureStage, AmbientLight, PointLight + +import simplepbr +import gltf class Game(ShowBase): def __init__(self, packages): ShowBase.__init__(self) - #self.disableMouse() - self.fs = Filesystem() - for package in packages: - self.fs.include(package) - - mdl2 = self.fs.find_model('$/arrangements/arrange1.glb') - mdl2.reparentTo(render) - mdl2.setTexture(self.fs.find_texture('$/sfsd.png'), 1) - - mdl_missing = self.fs.find_model('$/kakahead') - mdl_missing.reparentTo(render) + simplepbr.init() + gltf.patch_loader(self.loader) + + self.camLens.setFov(90) + self.camera.setPos(0, -4, 1) winprops = WindowProperties() winprops.setSize(1600, 900) self.win.requestProperties(winprops) -def start_game(packages): - if not packages: - packages = [] - game = Game(packages) - game.run() \ No newline at end of file + diff --git a/source/inspector.py b/source/inspector.py new file mode 100644 index 0000000..02a14b9 --- /dev/null +++ b/source/inspector.py @@ -0,0 +1,33 @@ + +from .packages import PackageManager +from direct.showbase.ShowBase import ShowBase +from panda3d.core import WindowProperties, TextureStage + +class Game(ShowBase): + def __init__(self, packages): + ShowBase.__init__(self) + #self.disableMouse() + + self.fs = PackageManager() + for package in packages: + self.fs.include(package) + + mdl2 = self.fs.find_model('$/arrangements/arrange3.glb') + mdl2.reparentTo(render) + mdl2.find('origin/wall').setTexture(self.fs.find_texture('$/generic_noise.png')) + + mdl_missing = self.fs.find_model('main-assetsz/sword1/sword1.glb') + print(mdl_missing) + mdl_missing.reparentTo(render) + + winprops = WindowProperties() + winprops.setSize(1600, 900) + self.win.requestProperties(winprops) + +def start_game(packages): + if not packages: + packages = [] + game = Game(packages) + game.run() + + diff --git a/source/packages/__init__.py b/source/packages/__init__.py new file mode 100644 index 0000000..f793f41 --- /dev/null +++ b/source/packages/__init__.py @@ -0,0 +1,2 @@ + +from .manager import PackageManager \ No newline at end of file diff --git a/source/packages/errors.py b/source/packages/errors.py new file mode 100644 index 0000000..057cb47 --- /dev/null +++ b/source/packages/errors.py @@ -0,0 +1,14 @@ +class FilepathNotFound(Exception): + pass + +class GamePackageMissing(Exception): + pass + +class GamePackageMalformed(Exception): + pass + +class PackageAlreadyLoaded(Exception): + pass + +class InvalidPackageType(Exception): + pass \ No newline at end of file diff --git a/source/packages/manager.py b/source/packages/manager.py new file mode 100644 index 0000000..a6c684b --- /dev/null +++ b/source/packages/manager.py @@ -0,0 +1,41 @@ + +import os, json +from .package import Package +from .errors import * + +class PackageManager: + def __init__(self, vfs): + self.vfs = vfs + self.package_map = {} + self.package_list = [] + self.main_package = None + + def get_package_meta(self, filepath): + if not os.path.exists(filepath): + raise FilepathNotFound(filepath) + gamepkg_path = os.path.join(filepath, 'gamepackage.json') + if not os.path.exists(gamepkg_path): + raise GamePackageMissing(gamepkg_path) + meta = json.load(open(gamepkg_path)) + if 'name' not in meta or 'type' not in meta: + raise GamePackageMalformed(gamepkg_path) + return meta + + def set_main_package(self, filepath): + meta = self.get_package_meta(filepath) + if meta['type'] != 'main': + raise InvalidPackageType(filepath, meta['type']) + vfs_path = f'/main/' + self.vfs.mount(filepath, vfs_path, 0) + new_package = Package(type('', (object,), meta), vfs_path, self.vfs) + self.main_package = new_package + + def include_package(self, filepath): + meta = self.get_package_meta(filepath) + if meta['name'] in self.package_map: + raise PackageAlreadyLoaded(meta['name']) + vfs_path = f'/include/{meta["name"]}' + self.vfs.mount(filepath, vfs_path, 0) + new_package = Package(type('', (object,), meta), vfs_path, self.vfs) + self.package_map[meta['name']] = new_package + self.package_list.insert(0, new_package) \ No newline at end of file diff --git a/source/packages/package.py b/source/packages/package.py new file mode 100644 index 0000000..a022712 --- /dev/null +++ b/source/packages/package.py @@ -0,0 +1,32 @@ + +import posixpath +from direct.stdpy.file import walk + +class Package: + def __init__(self, meta, mount_dir, vfs): + self.meta = meta + self.mount_dir = mount_dir + self.vfs = vfs + + def all_models(self): + models_path = posixpath.join(self.mount_dir, 'models') + output = [] + for root, paths, files in walk(models_path): + rel_root = root.replace(f'{models_path}', '').strip() + if len(rel_root) > 0 and rel_root[0] == '/': + rel_root = rel_root[1:] + for filepath in files: + output.append(posixpath.join(rel_root, filepath)) + return output + + def all_textures(self): + textures_path = posixpath.join(self.mount_dir, 'textures') + output = [] + for root, paths, files in walk(textures_path): + rel_root = root.replace(f'{textures_path}', '').strip() + if len(rel_root) > 0 and rel_root[0] == '/': + rel_root = rel_root[1:] + for filepath in files: + output.append(posixpath.join(rel_root, filepath)) + return output + diff --git a/source/pkgexplore.py b/source/pkgexplore.py new file mode 100644 index 0000000..ed3dcc0 --- /dev/null +++ b/source/pkgexplore.py @@ -0,0 +1,191 @@ + +import posixpath +import tkinter as tk +import tkinter.ttk as ttk +from source.packages import PackageManager +from direct.showbase.ShowBase import ShowBase +from panda3d.core import VirtualFileSystem, Material +from direct.stdpy.file import walk + +import simplepbr +import gltf + +def get_icons(): + return type('', (object,), { + 'app': tk.PhotoImage(file="icons/application.png"), + 'brick': tk.PhotoImage(file="icons/brick.png"), + 'bricks': tk.PhotoImage(file="icons/bricks.png"), + 'computer': tk.PhotoImage(file="icons/computer.png"), + 'folder': tk.PhotoImage(file="icons/folder.png"), + 'shape_group': tk.PhotoImage(file="icons/shape_group.png"), + 'pictures': tk.PhotoImage(file="icons/pictures.png"), + 'picture': tk.PhotoImage(file='icons/picture.png'), + "page_red": tk.PhotoImage(file='icons/page_red.png'), + 'shape_square': tk.PhotoImage(file='icons/shape_square.png'), + 'color_wheel': tk.PhotoImage(file='icons/color_wheel.png'), + 'color_swatch': tk.PhotoImage(file='icons/color_swatch.png') + })() + +class PandaApp(ShowBase): + def __init__(self, fStartDirect=True, windowType=None): + super().__init__(fStartDirect, windowType) + +running = True +def on_tk_closed(): + global running + running = False + +def add_package_contents_to_tree(tree, package, type, name, extensions, icon_folder, icon_group, icon_item): + data_path = posixpath.join(package.mount_dir, type) + tree.insert(package.mount_dir, 'end', data_path, text=f' {name}', image=icon_group) + file_count = 0 + for root, paths, files in walk(data_path): + for path in paths: + tree.insert(root, 'end', posixpath.join(root, path), text=f' {path}', image=icon_folder) + for file in files: + if file.lower().endswith(extensions): + file_count += 1 + tree.insert(root, 'end', posixpath.join(root, file), text=f' {file}', image=icon_item) + tree.item(data_path, text=f' {name} ({file_count})') + +def add_package_models_to_tree(tree, package, icons): + add_package_contents_to_tree(tree, package, 'models/', '3D Models', ('.glb', '.gltf', '.obj', '.egg'), icons.folder, icons.shape_group, icons.shape_square) + +def add_package_textures_to_tree(tree, package, icons): + add_package_contents_to_tree(tree, package, 'textures/', 'Image Textures', ('.png', '.jpg', '.bmp'), icons.folder, icons.pictures, icons.picture) + +def add_package_materials_to_tree(tree, package, icons): + add_package_contents_to_tree(tree, package, 'materials/', 'Materials', ('.json'), icons.folder, icons.color_wheel, icons.color_swatch) + +def add_package_to_tree(tree, package, icons): + add_package_models_to_tree(tree, package, icons) + add_package_textures_to_tree(tree, package, icons) + add_package_materials_to_tree(tree, package, icons) + +def create_tk_treeview(parent, packages, icons): + tree = ttk.Treeview(parent, show="tree") + tree.insert('', 'end', '/', text=' /', open=True, image=icons.app) + tree.insert('/', 'end', '/main/', text=f' Main Package "{packages.main_package.meta.name}" - {packages.main_package.meta.version}', image=icons.computer) + tree.insert('/', 'end', '/include/', text=f' Included Packages ({len(packages.package_list)})', image=icons.bricks) + add_package_to_tree(tree, packages.main_package, icons) + for package in packages.package_list: + tree.insert('/include/', 'end', package.mount_dir, text=f' "{package.meta.name}" - {package.meta.version}', image=icons.brick) + add_package_to_tree(tree, package, icons) + #tree.pack(fill='x') + return tree + +class TreeEvents: + def __init__(self, tree, panda): + self.tree = tree + self.panda = panda + self.tree.bind('', self.on_double_click) + self.nodes = [] + + def on_double_click(self, event): + item = self.tree.selection()[0] + if item.lower().endswith(('.glb', '.gltf', '.obj', '.egg')): + mat = Material() + mat.setAmbient((1, 0, 0, 1)) + node = self.panda.loader.loadModel(item) + node.reparentTo(self.panda.render) + node.setMaterial(mat) + self.nodes.append(node) + +def start_explorer(args): + tk_app = tk.Tk() + tk_app.geometry("700x500") + tk_app.protocol('WM_DELETE_WINDOW', on_tk_closed) + + #tk_app = tk.Frame(tk_app, width=500) + tk_app.grid_rowconfigure(0, weight=1) + tk_app.grid_columnconfigure(0, weight=1) + tk_app.grid_columnconfigure(1, weight=1) + + package_frame = tk.Frame(tk_app, highlightbackground="black", highlightthickness=1, padx=4, pady=4) + package_frame.grid(column=0, row=0, sticky="news") + package_frame.grid_columnconfigure(0, weight=1) + package_frame.grid_rowconfigure(0, weight=1) + package_frame.grid_rowconfigure(1, weight=1000) + label1 = tk.Label(package_frame, text='Packages') + label1.grid(row=0, column=0, sticky='n') + + scene_frame = tk.Frame(tk_app, highlightbackground="black", highlightthickness=1, padx=4, pady=4) + scene_frame.grid(column=1, row=0, sticky="news") + scene_frame.grid_columnconfigure(0, weight=1) + scene_frame.grid_rowconfigure(0, weight=1) + scene_frame.grid_rowconfigure(1, weight=10000) + + label2 = tk.Label(scene_frame, text='Scene Graph') + label2.grid(row=0, column=0) + + scene_tree = ttk.Treeview(scene_frame) + scene_tree.grid(row=1, column=0, sticky="news") + + panda_app = PandaApp() + simplepbr.init() + gltf.patch_loader(panda_app.loader) + + packages = PackageManager(VirtualFileSystem.getGlobalPtr()) + packages.set_main_package(args.main) + if args.package: + for include in args.package: + packages.include_package(include) + + icons = get_icons() + tree = create_tk_treeview(package_frame, packages, icons) + tree.grid(row=1, column=0, sticky="news") + + events = TreeEvents(tree, panda_app) + + try: + while running: + panda_app.taskMgr.step() + tk_app.update_idletasks() + tk_app.update() + except KeyboardInterrupt as stop: + pass + + ''' + window = tk.Tk() + icons = get_icons() + + fs = PackageManager(VirtualFileSystem.getGlobalPtr()) + fs.set_main_package('assets') + + mdls = fs.main_package.all_models() + txts = fs.main_package.all_textures() + print(mdls, txts, loader) + + menu = tk.Menu(window) + menu.add_command(label="Exit", command=window.quit) + window.config(menu=menu) + + mainframe = tk.Frame(window, borderwidth=5) + mainframe.grid(column=0, row=0) + mainframe.columnconfigure(0, weight=3) + + packageTree = ttk.Treeview(mainframe, show="tree") + packageTree.insert('', 'end', '/', text=' Virtual FS', image=icons.app) + packageTree.pack() + + window.mainloop() + frame = tk.Frame(window, borderwidth=5) + frame.pack() + + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + + tree = ttk.Treeview(frame, show="tree") + tree.grid(column=0, row=0, sticky=tk.NW) + + tree.insert('', 'end', '/', text=' Virtual Filesystem', open=True, image=icons.app) + tree.insert('/', 'end', '/main/', text=' main/', image=icons.computer) + tree.insert('/main/', 'end', '/main/models/', text=' models/', image=icons.folder) + tree.insert('/main/', 'end', '/main/textures/', text=' textures/', image=icons.folder) + tree.insert('/', 'end', '/packages/', text=' packages/', image=icons.bricks) + tree.insert('/packages/', 'end', '/packages/user-package-1/', text=' user-package-1/', image=icons.brick) + tree.insert('/packages/', 'end', '/packages/user-package-2/', text=' user-package-2/', image=icons.brick) + tree.insert('/packages/', 'end', '/packages/user-package-3/', text=' user-package-3/', image=icons.brick) + + window.mainloop() + ''' \ No newline at end of file diff --git a/testmod/gamepackage.json b/testmod/gamepackage.json new file mode 100644 index 0000000..dd464b8 --- /dev/null +++ b/testmod/gamepackage.json @@ -0,0 +1,5 @@ +{ + "name": "test-mod", + "version": "1.1-beta", + "type": "include" +} \ No newline at end of file diff --git a/testmod/models/axis.glb b/testmod/models/axis.glb new file mode 100644 index 0000000..d6e472b Binary files /dev/null and b/testmod/models/axis.glb differ diff --git a/testmod/models/box.glb b/testmod/models/box.glb new file mode 100644 index 0000000..55dd1dd Binary files /dev/null and b/testmod/models/box.glb differ diff --git a/testmod/models/missing.glb b/testmod/models/missing.glb new file mode 100644 index 0000000..d313646 Binary files /dev/null and b/testmod/models/missing.glb differ