├── ogre-meshviewer.bat ├── screenshot.jpg ├── fonts ├── Ubuntu-B.ttf ├── UbuntuMono-R.ttf ├── forkawesome-webfont.ttf └── meshviewer.fontdef ├── pylintrc.toml ├── snap └── gui │ ├── ogre-meshviewer.desktop │ └── icon.svg ├── win_resources.cfg ├── test └── cube.obj ├── .github └── workflows │ └── ci-build.yml ├── LICENSE ├── README.md ├── create_win_package.sh ├── snapcraft.yaml └── ogre_mesh_viewer.py /ogre-meshviewer.bat: -------------------------------------------------------------------------------- 1 | cd /d %~dp0 2 | start pythonw ogre_mesh_viewer.py %1 3 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OGRECave/ogre-meshviewer/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /fonts/Ubuntu-B.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OGRECave/ogre-meshviewer/HEAD/fonts/Ubuntu-B.ttf -------------------------------------------------------------------------------- /fonts/UbuntuMono-R.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OGRECave/ogre-meshviewer/HEAD/fonts/UbuntuMono-R.ttf -------------------------------------------------------------------------------- /fonts/forkawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OGRECave/ogre-meshviewer/HEAD/fonts/forkawesome-webfont.ttf -------------------------------------------------------------------------------- /pylintrc.toml: -------------------------------------------------------------------------------- 1 | [tool.pylint] 2 | extension-pkg-allow-list = "Ogre" 3 | disable = ["R", "C", "attribute-defined-outside-init", "redefined-builtin", "redefined-outer-name", "unused-argument", "protected-access"] 4 | enable = ["consider-using-f-string", "singleton-comparison"] 5 | -------------------------------------------------------------------------------- /snap/gui/ogre-meshviewer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=OGRE Mesh Viewer 4 | GenericName=Mesh Viewer 5 | Keywords=viewer;mesh; 6 | MimeType=model/x.stl-binary; 7 | Exec=ogre-meshviewer %f 8 | Path=${SNAP}/bin/ 9 | Icon=${SNAP}/meta/gui/icon.svg 10 | Terminal=false 11 | Categories=Utility; 12 | -------------------------------------------------------------------------------- /win_resources.cfg: -------------------------------------------------------------------------------- 1 | # This file is only used on Windows, where we cannot simply reference the Ogre installation 2 | [OgreInternal] 3 | FileSystem=Main 4 | FileSystem=RTShaderLib 5 | 6 | [OgreMeshViewer] 7 | Zip=./SdkTrays.zip 8 | 9 | [General] 10 | # Bites uses the next entry to discover the platform shaders 11 | FileSystem=. -------------------------------------------------------------------------------- /test/cube.obj: -------------------------------------------------------------------------------- 1 | # Blender 4.0.2 2 | # www.blender.org 3 | o Cube 4 | v 1.000000 1.000000 -1.000000 5 | v 1.000000 -1.000000 -1.000000 6 | v 1.000000 1.000000 1.000000 7 | v 1.000000 -1.000000 1.000000 8 | v -1.000000 1.000000 -1.000000 9 | v -1.000000 -1.000000 -1.000000 10 | v -1.000000 1.000000 1.000000 11 | v -1.000000 -1.000000 1.000000 12 | s 0 13 | f 1 5 7 3 14 | f 4 3 7 8 15 | f 8 7 5 6 16 | f 6 2 4 8 17 | f 2 1 3 4 18 | f 6 5 1 2 19 | -------------------------------------------------------------------------------- /fonts/meshviewer.fontdef: -------------------------------------------------------------------------------- 1 | font Icons 2 | { 3 | type truetype 4 | source forkawesome-webfont.ttf 5 | size 15 6 | resolution 96 7 | code_points 0xf000-0xf2e0 8 | } 9 | 10 | font UIText 11 | { 12 | type truetype 13 | source Ubuntu-B.ttf 14 | size 15 15 | resolution 96 16 | merge_fonts Icons 17 | } 18 | 19 | font LogText 20 | { 21 | type truetype 22 | source UbuntuMono-R.ttf 23 | size 15 24 | resolution 96 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: CI Build 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | jobs: 8 | linux: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Install Dependencies 12 | run: | 13 | sudo apt update 14 | sudo apt install -y python3-pip python3-tk xvfb libxrandr2 libegl1 15 | pip3 install ogre-python pylint 16 | - uses: actions/checkout@v4 17 | - name: Test 18 | run: | 19 | pylint *.py 20 | cd test 21 | xvfb-run timeout --preserve-status 5s python3 ../ogre_mesh_viewer.py cube.obj -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pavel Rojtberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ogre-meshviewer 2 | 3 | 4 | 5 | Viewer for `.mesh` model files as consumed by OGRE as well as any format [supported by assimp](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md) like `.obj`, `.ply` or `.fbx`. 6 | 7 | ![](screenshot.jpg) 8 | 9 | # Features 10 | * Display mesh properties (bounds, referenced materials) 11 | * Highlight submeshes in 3D view 12 | * Preview linked animations (skeleton and vertex) 13 | * Easy to use UI 14 | 15 | # Download 16 | [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/ogre-meshviewer) 17 | 18 | [Portable Windows Package](https://github.com/OGRECave/ogre-meshviewer/releases) 19 | 20 | # Dependencies 21 | * [ogre-python](https://pypi.org/project/ogre-python/) >= 14.3 22 | * python3 23 | 24 | # Usage 25 | Double click on `.mesh` in file browser or use the CLI as 26 | ``` 27 | ogre-meshviewer [-h] [-c RESCFG] meshfile 28 | ``` 29 | where `meshfile` can be either an absolute path or a resource name referenced in RESCFG. 30 | 31 | -------------------------------------------------------------------------------- /create_win_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This should work on Windows (MinGW) and Linux 4 | # for MinGW use e.g. https://gitforwindows.org/ 5 | 6 | mkdir winpkg 7 | cd winpkg 8 | 9 | curl -L https://dl.cloudsmith.io/public/ogrecave/ogre/raw/versions/v14.4.0/ogre-sdk-v14.4.0-msvc142-x64.zip -o ogre-sdk.zip 10 | curl -LO https://www.python.org/ftp/python/3.10.10/python-3.10.10-embed-amd64.zip 11 | curl -LO https://github.com/OGRECave/ogre-meshviewer/releases/download/24.02/tkinter_3.10.10-amd64.zip 12 | unzip python-3.10.10-embed-amd64.zip -d package 13 | unzip tkinter_3.10.10-amd64.zip -d package 14 | unzip ogre-sdk.zip 15 | 16 | # main 17 | cp ../ogre*py ../ogre*bat ../LICENSE ../README.md package/ 18 | 19 | # copy ogre parts 20 | cp -R lib/python3.10/dist-packages/Ogre package 21 | # components 22 | cp bin/OgreMain.dll bin/OgreBites.dll bin/OgreOverlay.dll bin/OgreRTShaderSystem.dll bin/OgreTerrain.dll bin/OgrePaging.dll package 23 | # plugins 24 | cp bin/Codec*dll bin/RenderSystem*dll bin/Plugin_DotScene.dll bin/Plugin_GLSLangProgramManager.dll package 25 | # deps 26 | cp bin/SDL2.dll bin/zlib.dll package 27 | 28 | # write plugins.cfg 29 | head -10 bin/plugins.cfg > package/plugins.cfg 30 | echo Plugin=Codec_RsImage >> package/plugins.cfg 31 | echo Plugin=Codec_Assimp >> package/plugins.cfg 32 | echo Plugin=Plugin_DotScene >> package/plugins.cfg 33 | echo Plugin=RenderSystem_Vulkan >> package/plugins.cfg 34 | echo Plugin=Plugin_GLSLangProgramManager >> package/plugins.cfg 35 | 36 | # resources 37 | cp ../win_resources.cfg package/resources.cfg 38 | cp -R Media/RTShaderLib Media/Main package/ 39 | cp -R ../fonts package/ 40 | 41 | mv package ogre-meshviewer_25.07-win64 42 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ogre-meshviewer 2 | version: "25.07" 3 | confinement: strict 4 | summary: OGRE Mesh Viewer 5 | description: Viewer for .mesh model files as consumed by OGRE 6 | base: core22 7 | 8 | apps: 9 | ogre-meshviewer: 10 | command: bin/desktop-launch python3 $SNAP/bin/ogre_mesh_viewer.py 11 | plugs: [home, opengl, x11, removable-media] 12 | environment: 13 | PYTHONPATH: $SNAP/usr/lib/python3.10/:$SNAP/usr/lib/python3.10/dist-packages/:$SNAP/usr/lib/python3.10/lib-dynload 14 | TCL_LIBRARY: $SNAP/usr/share/tcltk/tcl8.6 15 | parts: 16 | desktop-glib-only: 17 | source: https://github.com/ubuntu/snapcraft-desktop-helpers.git 18 | source-subdir: glib-only 19 | plugin: make 20 | ogre: 21 | plugin: cmake 22 | cmake-parameters: 23 | - -DCMAKE_BUILD_TYPE=Release 24 | - -DOGRE_BUILD_DEPENDENCIES=FALSE 25 | - -DOGRE_BUILD_RENDERSYSTEM_GL3PLUS=TRUE 26 | - -DOGRE_BUILD_RENDERSYSTEM_GL=TRUE 27 | - -DOGRE_BUILD_RENDERSYSTEM_GLES2=TRUE 28 | - -DOGRE_BUILD_PLUGIN_RSIMAGE=TRUE 29 | - -DOGRE_BUILD_COMPONENT_PYTHON=TRUE 30 | - -DOGRE_BUILD_PLUGIN_DOT_SCENE=TRUE 31 | - -DOGRE_BUILD_COMPONENT_TERRAIN=TRUE 32 | - -DOGRE_BUILD_COMPONENT_PAGING=TRUE 33 | - -DPYTHON_EXECUTABLE=/usr/bin/python3 34 | # stuff we dont need for the viewer 35 | - -DOGRE_BUILD_TOOLS=FALSE 36 | - -DOGRE_BUILD_SAMPLES=FALSE 37 | - -DOGRE_BUILD_PLUGIN_STBI=FALSE # we want to use rsimage instead 38 | - -DOGRE_BUILD_PLUGIN_FREEIMAGE=FALSE 39 | - -DOGRE_BUILD_PLUGIN_EXRCODEC=FALSE 40 | - -DOGRE_BUILD_PLUGIN_BSP=FALSE 41 | - -DOGRE_BUILD_PLUGIN_PCZ=FALSE 42 | - -DOGRE_BUILD_PLUGIN_OCTREE=FALSE 43 | - -DOGRE_BUILD_COMPONENT_JAVA=FALSE 44 | - -DOGRE_BUILD_COMPONENT_CSHARP=FALSE 45 | - -DOGRE_BUILD_COMPONENT_VOLUME=FALSE 46 | - -DOGRE_BUILD_COMPONENT_PROPERTY=FALSE 47 | - -DOGRE_BUILD_COMPONENT_MESHLODGENERATOR=FALSE 48 | - -DCMAKE_INSTALL_PREFIX=/usr/ 49 | - -DCMAKE_CXX_COMPILER=/usr/bin/clang++-15 50 | source: https://github.com/OGRECave/ogre.git 51 | source-tag: v14.4.0 52 | source-depth: 1 53 | build-packages: 54 | - libassimp-dev 55 | - libxrandr-dev 56 | - libfreetype6-dev 57 | - libgles2-mesa-dev 58 | - libsdl2-dev 59 | - libpython3-dev 60 | - libpugixml-dev 61 | - swig4.0 62 | - clang-15 63 | - cargo 64 | - on amd64: [nvidia-cg-dev] 65 | stage-packages: 66 | - libassimp5 67 | - libfreetype6 68 | - libsdl2-2.0-0 69 | - python3 70 | - python3-tk 71 | - libpython3.10 72 | - libpugixml1v5 73 | - libgl1 74 | - libsm6 75 | - libgles2 76 | - libegl1 77 | - on amd64: [libcggl] 78 | viewer: 79 | plugin: dump 80 | source: https://github.com/OGRECave/ogre-meshviewer.git 81 | organize: 82 | ogre_mesh_viewer.py: bin/ 83 | fonts: bin/fonts 84 | stage: 85 | - bin/ 86 | after: [ogre, desktop-glib-only] 87 | -------------------------------------------------------------------------------- /snap/gui/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 58 | 59 | 60 | Pierre Fontaine 61 | 62 | 63 | https://forums.ogre3d.org/viewtopic.php?f=4&t=84127&start=75#p524164 64 | 65 | 67 | 69 | 71 | 73 | 75 | 77 | 78 | 79 | 80 | 85 | 92 | 96 | 100 | 106 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /ogre_mesh_viewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os.path 4 | import time 5 | 6 | import tkinter as tk 7 | from tkinter import filedialog 8 | 9 | import Ogre 10 | import Ogre.RTShader as OgreRTShader 11 | import Ogre.Bites as OgreBites 12 | import Ogre.Overlay 13 | import Ogre.ImGui as ImGui 14 | 15 | RGN_MESHVIEWER = "OgreMeshViewer" 16 | RGN_USERDATA = "UserData" 17 | 18 | VES2STR = ("ERROR", "Position", "Blend Weights", "Blend Indices", "Normal", "Diffuse", "Specular", "Texcoord", "Binormal", "Tangent") 19 | VET2STR = ("float", "float2", "float3", "float4", "ERROR", 20 | "short", "short2", "short3", "short4", "ubyte4", "argb", "abgr", 21 | "double", "double2", "double3", "double4", 22 | "ushort", "ushort2", "ushort3", "ushort4", 23 | "int", "int2", "int3", "int4", 24 | "uint", "uint2", "uint3", "uint4", 25 | "byte4", "byte4n", "ubyte4n", "short2n", "short4n", "ushort2n", "ushort4n", "int1010102n", 26 | "half", "half2", "half3", "half4") 27 | 28 | ROP2STR = ("ERROR", "Point List", "Line List", "Line Strip", "Triangle List", "Triangle Strip", "Triangle Fan") 29 | 30 | def show_vertex_decl(decl): 31 | flags = ImGui.TableFlags_Borders | ImGui.TableFlags_SizingStretchProp 32 | if not ImGui.BeginTable("vertexDecl", 3, flags): 33 | return 34 | ImGui.TableSetupColumn("Semantic") 35 | ImGui.TableSetupColumn("Type") 36 | ImGui.TableSetupColumn("Buffer") 37 | ImGui.TableHeadersRow() 38 | 39 | for e in decl.getElements(): 40 | ImGui.TableNextRow() 41 | ImGui.TableNextColumn() 42 | ImGui.Text(VES2STR[e.getSemantic()]) 43 | ImGui.TableNextColumn() 44 | ImGui.Text(VET2STR[e.getType()]) 45 | ImGui.TableNextColumn() 46 | ImGui.Text(str(e.getSource())) 47 | ImGui.EndTable() 48 | 49 | _rgbcol = ((1, 0.6, 0.6, 1), (0.6, 1, 0.6, 1), (0.6, 0.6, 1, 1)) 50 | _xyzstr = ("X {0:.2f}", "Y {0:.2f}", "Z {0:.2f}") 51 | def draw_lbl_table_row(lbl, val, valstr = _xyzstr, valcol = _rgbcol): 52 | ImGui.TableNextRow() 53 | ImGui.TableNextColumn() 54 | ImGui.Text(lbl) 55 | for s, v, c in zip(valstr, val, valcol): 56 | ImGui.TableNextColumn() 57 | if v is None: 58 | continue 59 | ImGui.PushStyleColor(ImGui.Col_Text, ImGui.ImVec4(*c)) 60 | ImGui.Text(s.format(v)) 61 | ImGui.PopStyleColor() 62 | 63 | def printable(str): 64 | return str.encode("utf-8", "replace").decode() 65 | 66 | def askopenfilename(initialdir=None): 67 | infile = filedialog.askopenfilename( 68 | title="Select Mesh File", 69 | initialdir=initialdir, 70 | filetypes=[("All files", "*"), 71 | ("Ogre files", "*.mesh *.scene"), 72 | ("Common mesh files", "*.obj *.fbx *.ply *.gltf *.glb ")]) 73 | return infile 74 | 75 | class GridFloor: 76 | def __init__(self, scale, parent_node): 77 | self.material = Ogre.MaterialManager.getSingleton().create("VertexColour", RGN_MESHVIEWER) 78 | p = self.material.getTechnique(0).getPass(0) 79 | p.setLightingEnabled(False) 80 | p.setVertexColourTracking(Ogre.TVC_AMBIENT) 81 | 82 | self.plane_node = parent_node.createChildSceneNode() 83 | self.plane_node.setScale(scale, scale, scale) 84 | 85 | self.planes = [self._create_plane(i) for i in range(3)] 86 | 87 | def show_plane(self, plane): 88 | for i, grid in enumerate(self.planes): 89 | grid.setVisible(i == plane) 90 | 91 | def _create_plane(self, plane): 92 | normal = [0, 0, 0] 93 | normal[plane] = 1 94 | 95 | axis_color = [[0, 0, 0], [0, 0, 0]] 96 | axis_color[0][(plane + 2) % 3] = 1 97 | axis_color[1][(plane + 1) % 3] = 1 98 | 99 | grid_color = (0.2, 0.2, 0.2) 100 | 101 | # Compute the other axes based on the normal vector 102 | axis = [Ogre.Vector3(normal[1], normal[2], normal[0]), 103 | Ogre.Vector3(normal[2], normal[0], normal[1])] 104 | 105 | o = self.plane_node.getCreator().createManualObject(f"MeshViewer/plane{plane}") 106 | o.begin(self.material, Ogre.RenderOperation.OT_LINE_LIST) 107 | o.setQueryFlags(0) 108 | o.setVisible(False) 109 | 110 | for i in range(2): 111 | for j in range(-5, 6): 112 | cl = axis_color[i] if j == 0 else grid_color 113 | o.position(-axis[i] + axis[1 - i] * j/5) 114 | o.colour(cl) 115 | o.position(axis[i] + axis[1 - i] * j/5) 116 | o.colour(cl) 117 | 118 | o.end() 119 | 120 | self.plane_node.attachObject(o) 121 | return o 122 | 123 | class MaterialCreator(Ogre.MeshSerializerListener): 124 | 125 | def __init__(self): 126 | Ogre.MeshSerializerListener.__init__(self) 127 | 128 | def processMaterialName(self, mesh, name): 129 | # ensure some material exists so we can display the name 130 | mat_mgr = Ogre.MaterialManager.getSingleton() 131 | if not mat_mgr.resourceExists(name, mesh.getGroup()): 132 | lmgr = Ogre.LogManager.getSingleton() 133 | try: 134 | mat = mat_mgr.create(name, mesh.getGroup()) 135 | lmgr.logWarning(f"could not find material '{printable(mat.getName())}'") 136 | except RuntimeError: 137 | # do not crash if name is "" 138 | # this is illegal due to OGRE specs, but we want to show that in the UI 139 | lmgr.logError("sub-mesh uses empty material name") 140 | 141 | def processSkeletonName(self, mesh, name): pass 142 | 143 | def processMeshCompleted(self, mesh): pass 144 | 145 | class LogWindow(Ogre.LogListener): 146 | def __init__(self): 147 | Ogre.LogListener.__init__(self) 148 | 149 | self.show = False 150 | self.items = [] 151 | 152 | self.font = None 153 | 154 | def messageLogged(self, msg, lvl, *args): 155 | ts = time.strftime("%T", time.localtime()) 156 | self.items.append((ts, printable(msg.replace("%", "%%")), lvl)) 157 | 158 | def draw(self): 159 | if not self.show: 160 | return 161 | 162 | ImGui.SetNextWindowSize(ImGui.ImVec2(500, 400), ImGui.Cond_FirstUseEver) 163 | self.show = ImGui.Begin("Log", self.show)[1] 164 | 165 | ImGui.PushFont(self.font) 166 | for ts, msg, lvl in self.items: 167 | ImGui.PushStyleColor(ImGui.Col_Text, ImGui.ImVec4(0.6, 0.6, 0.6, 1)) 168 | ImGui.Text(ts) 169 | ImGui.PopStyleColor() 170 | ImGui.SameLine() 171 | if lvl == 4: 172 | ImGui.PushStyleColor(ImGui.Col_Text, ImGui.ImVec4(1, 0.4, 0.4, 1)) 173 | elif lvl == 3: 174 | ImGui.PushStyleColor(ImGui.Col_Text, ImGui.ImVec4(1, 0.8, 0.4, 1)) 175 | ImGui.TextWrapped(msg) 176 | if lvl > 2: 177 | ImGui.PopStyleColor() 178 | ImGui.PopFont() 179 | ImGui.End() 180 | 181 | class MeshViewerGui(Ogre.RenderTargetListener): 182 | 183 | def __init__(self, app): 184 | Ogre.RenderTargetListener.__init__(self) 185 | self.show_about = False 186 | self.show_metrics = False 187 | self.show_render_settings = False 188 | self.side_panel_visible = True 189 | 190 | self.app = app 191 | 192 | self.highlighted = -1 193 | self.orig_mat = None 194 | self.logwin = app.logwin 195 | 196 | self.lod_idx_override = -1 197 | 198 | def draw_about(self): 199 | flags = ImGui.WindowFlags_AlwaysAutoResize 200 | self.show_about = ImGui.Begin("About OgreMeshViewer", self.show_about, flags)[1] 201 | ImGui.TextLinkOpenURL("OgreMeshViewer", "https://github.com/OGRECave/ogre-meshviewer") 202 | ImGui.SameLine(0, 0) 203 | ImGui.Text(" is licensed under the ") 204 | ImGui.SameLine(0, 0) 205 | ImGui.TextLinkOpenURL("MIT License.", "https://github.com/OGRECave/ogre-meshviewer/blob/master/LICENSE") 206 | ImGui.Text("by Pavel Rojtberg and contributors.") 207 | ImGui.Separator() 208 | ImGui.BulletText(f"Ogre: {Ogre.__version__}") 209 | ImGui.BulletText(f"ImGui: {ImGui.GetVersion()}") 210 | ImGui.End() 211 | 212 | def draw_render_settings(self): 213 | flags = ImGui.WindowFlags_AlwaysAutoResize 214 | self.show_render_settings = ImGui.Begin("Renderer Settings", self.show_render_settings, flags)[1] 215 | 216 | app.next_rendersystem = Ogre.Overlay.DrawRenderingSettings(app.next_rendersystem) 217 | 218 | ImGui.Separator() 219 | 220 | if ImGui.Button("Apply & Restart"): 221 | app.restart = True 222 | app.getRoot().queueEndRendering() 223 | 224 | ImGui.End() 225 | 226 | def draw_metrics(self): 227 | win = self.app.getRenderWindow() 228 | stats = win.getStatistics() 229 | 230 | ImGui.SetNextWindowPos(ImGui.ImVec2(win.getWidth() - 10, win.getHeight() - 10), ImGui.Cond_Always, ImGui.ImVec2(1, 1)) 231 | ImGui.SetNextWindowBgAlpha(0.3) 232 | flags = ImGui.WindowFlags_NoMove | ImGui.WindowFlags_NoTitleBar | ImGui.WindowFlags_NoResize | \ 233 | ImGui.WindowFlags_AlwaysAutoResize | ImGui.WindowFlags_NoSavedSettings | ImGui.WindowFlags_NoFocusOnAppearing | \ 234 | ImGui.WindowFlags_NoNav 235 | self.show_metrics = ImGui.Begin("Metrics", self.show_metrics, flags)[1] 236 | 237 | stats_dict = { 238 | "Average FPS": f"{stats.avgFPS:.2f}", 239 | "Batches": f"{stats.batchCount}", 240 | "Triangles": f"{stats.triangleCount}" 241 | } 242 | 243 | ImGui.Text("Metrics") 244 | ImGui.Separator() 245 | if ImGui.BeginTable("Metrics", 2): 246 | for stat, value in stats_dict.items(): 247 | ImGui.TableNextRow() 248 | ImGui.TableSetColumnIndex(0) 249 | ImGui.Text(stat) 250 | ImGui.TableSetColumnIndex(1) 251 | ImGui.Text(value) 252 | ImGui.EndTable() 253 | ImGui.End() 254 | 255 | def draw_loading(self): 256 | win = self.app.getRenderWindow() 257 | ImGui.SetNextWindowPos(ImGui.ImVec2(win.getWidth() * 0.5, win.getHeight() * 0.5), 0, ImGui.ImVec2(0.5, 0.5)) 258 | 259 | flags = ImGui.WindowFlags_NoTitleBar | ImGui.WindowFlags_NoResize | ImGui.WindowFlags_NoSavedSettings 260 | ImGui.Begin("Loading", True, flags) 261 | ImGui.Text(self.app.filename) 262 | ImGui.Separator() 263 | ImGui.Text("\uf252 Loading.. ") 264 | ImGui.End() 265 | 266 | def load_file(self): 267 | infile = askopenfilename(app.filedir) 268 | if not infile: 269 | return 270 | 271 | app.infile = infile 272 | app.reload() 273 | 274 | def preRenderTargetUpdate(self, evt): 275 | if not self.app.cam.getViewport().getOverlaysEnabled(): 276 | return 277 | 278 | Ogre.Overlay.ImGuiOverlay.NewFrame() 279 | 280 | entity = self.app.entity 281 | 282 | if entity is None and self.app.attach_node is None: 283 | self.draw_loading() 284 | return 285 | 286 | if ImGui.BeginMainMenuBar(): 287 | 288 | if ImGui.BeginMenu("File"): 289 | if ImGui.MenuItem("Open..", "F1"): 290 | self.load_file() 291 | if ImGui.MenuItem("Reload", "F5"): 292 | app.reload(keep_cam=True) 293 | if ImGui.MenuItem("Save Screenshot", "P"): 294 | self.app._save_screenshot() 295 | ImGui.Separator() 296 | if ImGui.MenuItem("Renderer Settings"): 297 | self.show_render_settings = True 298 | if ImGui.MenuItem("Quit", "Esc"): 299 | self.app.getRoot().queueEndRendering() 300 | ImGui.EndMenu() 301 | 302 | if ImGui.BeginMenu("View"): 303 | if ImGui.MenuItem("Side Panel", "N", self.side_panel_visible): 304 | self.side_panel_visible = not self.side_panel_visible 305 | if ImGui.BeginMenu("Fixed Camera Yaw"): 306 | if ImGui.MenuItem("Disabled", "", self.app.fixed_yaw_axis == -1): 307 | self.app.fixed_yaw_axis = -1 308 | self.app.update_fixed_camera_yaw() 309 | ImGui.Separator() 310 | if ImGui.MenuItem("X Axis", "", self.app.fixed_yaw_axis == 0): 311 | self.app.fixed_yaw_axis = 0 312 | self.app.update_fixed_camera_yaw() 313 | if ImGui.MenuItem("Y Axis", "", self.app.fixed_yaw_axis == 1): 314 | self.app.fixed_yaw_axis = 1 315 | self.app.update_fixed_camera_yaw() 316 | if ImGui.MenuItem("Z Axis", "", self.app.fixed_yaw_axis == 2): 317 | self.app.fixed_yaw_axis = 2 318 | self.app.update_fixed_camera_yaw() 319 | ImGui.EndMenu() 320 | if ImGui.MenuItem("Orthographic Projection", "KP5", self.app.cam.getProjectionType() == Ogre.PT_ORTHOGRAPHIC): 321 | self.app._toggle_projection() 322 | if ImGui.MenuItem("Wireframe Mode", "W", app.cam.getPolygonMode() == Ogre.PM_WIREFRAME): 323 | self.app._toggle_wireframe_mode() 324 | ImGui.EndMenu() 325 | 326 | if entity is not None and ImGui.BeginMenu("Overlay"): 327 | enode = entity.getParentSceneNode() 328 | if ImGui.MenuItem("Axes", "A", self.app.axes_visible): 329 | self.app._toggle_axes() 330 | if ImGui.MenuItem("Grid", "G", self.app.grid_visible): 331 | self.app._toggle_grid() 332 | if ImGui.MenuItem("Bounding Box", "B", enode.getShowBoundingBox()): 333 | self.app._toggle_bbox() 334 | if entity.hasSkeleton() and ImGui.MenuItem("Skeleton", None, entity.getDisplaySkeleton()): 335 | entity.setDisplaySkeleton(not entity.getDisplaySkeleton()) 336 | ImGui.EndMenu() 337 | 338 | if ImGui.BeginMenu("Help"): 339 | if ImGui.MenuItem("Metrics", None, self.show_metrics): 340 | self.show_metrics = not self.show_metrics 341 | if ImGui.MenuItem("Log"): 342 | self.logwin.show = True 343 | if ImGui.MenuItem("About"): 344 | self.show_about = True 345 | ImGui.EndMenu() 346 | 347 | ImGui.EndMainMenuBar() 348 | 349 | if self.show_about: 350 | self.draw_about() 351 | 352 | if self.show_metrics: 353 | self.draw_metrics() 354 | 355 | if self.show_render_settings: 356 | self.draw_render_settings() 357 | 358 | self.logwin.draw() 359 | 360 | if entity is None: 361 | # no sidebar yet when loading .scene 362 | return 363 | 364 | if self.side_panel_visible is False: 365 | # hide side panel 366 | return 367 | 368 | # Mesh Info Sidebar 369 | mesh = entity.getMesh() 370 | 371 | ImGui.SetNextWindowSize(ImGui.ImVec2(300, ImGui.GetFontSize()*25), ImGui.Cond_FirstUseEver) 372 | ImGui.SetNextWindowPos(ImGui.ImVec2(0, ImGui.GetFontSize()*1.5)) 373 | flags = ImGui.WindowFlags_NoTitleBar | ImGui.WindowFlags_NoMove 374 | ImGui.Begin("MeshProps", None, flags) 375 | ImGui.Text("\uf016 "+mesh.getName()) 376 | 377 | highlight = -1 378 | 379 | if ImGui.CollapsingHeader("Geometry"): 380 | if mesh.sharedVertexData: 381 | if ImGui.TreeNode(f"Shared Vertices: {mesh.sharedVertexData.vertexCount}"): 382 | show_vertex_decl(mesh.sharedVertexData.vertexDeclaration) 383 | ImGui.TreePop() 384 | else: 385 | ImGui.Text("Shared Vertices: None") 386 | 387 | for i, sm in enumerate(mesh.getSubMeshes()): 388 | submesh_details = ImGui.TreeNode(f"SubMesh #{i}") 389 | if ImGui.IsItemHovered(): 390 | highlight = i 391 | 392 | if submesh_details: 393 | ImGui.BulletText(f"Material: {printable(sm.getMaterialName())}") 394 | op = ROP2STR[sm.operationType] if sm.operationType <= 6 else "Control Points" 395 | ImGui.BulletText(f"Operation: {op}") 396 | 397 | if sm.indexData.indexCount: 398 | bits = sm.indexData.indexBuffer.getIndexSize() * 8 399 | ImGui.BulletText(f"Indices: {sm.indexData.indexCount} ({bits} bit)") 400 | else: 401 | ImGui.BulletText("Indices: None") 402 | 403 | if sm.vertexData: 404 | if ImGui.TreeNode(f"Vertices: {sm.vertexData.vertexCount}"): 405 | show_vertex_decl(sm.vertexData.vertexDeclaration) 406 | ImGui.TreePop() 407 | else: 408 | ImGui.BulletText("Vertices: shared") 409 | ImGui.TreePop() 410 | 411 | if mesh.getEdgeList(): 412 | ImGui.Text("\uf05a EdgeLists present") 413 | 414 | if self.highlighted > -1: 415 | entity.getSubEntities()[self.highlighted].setMaterialName(self.orig_mat) 416 | self.highlighted = -1 417 | 418 | if highlight > -1: 419 | self.orig_mat = printable(entity.getSubEntities()[highlight].getMaterial().getName()) 420 | entity.getSubEntities()[highlight].setMaterial(self.app.highlight_mat) 421 | self.highlighted = highlight 422 | 423 | animations = entity.getAllAnimationStates() 424 | if animations is not None and ImGui.CollapsingHeader("Animations"): 425 | controller_mgr = Ogre.ControllerManager.getSingleton() 426 | 427 | if entity.hasSkeleton(): 428 | ImGui.Text(f"\uf183 Skeleton: {mesh.getSkeletonName()}") 429 | # self.entity.setUpdateBoundingBoxFromSkeleton(True) 430 | if mesh.hasVertexAnimation(): 431 | ImGui.Text("\uf1e0 Vertex Animations") 432 | 433 | for name, astate in animations.getAnimationStates().items(): 434 | if ImGui.TreeNode(name): 435 | ImGui.PushID(name) 436 | if astate.getEnabled(): 437 | if ImGui.Button("\uf048 Reset"): 438 | astate.setEnabled(False) 439 | astate.setTimePosition(0) 440 | if name in self.app.active_controllers: 441 | controller_mgr.destroyController(self.app.active_controllers[name]) 442 | elif ImGui.Button("\uf04b Play"): 443 | astate.setEnabled(True) 444 | self.app.active_controllers[name] = controller_mgr.createFrameTimePassthroughController( 445 | Ogre.AnimationStateControllerValue.create(astate, True)) 446 | 447 | if astate.getLength() > 0: 448 | ImGui.SameLine() 449 | changed, value = ImGui.SliderFloat("", astate.getTimePosition(), 0, astate.getLength(), "%.3fs") 450 | if changed: 451 | astate.setEnabled(True) 452 | astate.setTimePosition(value) 453 | ImGui.PopID() 454 | ImGui.TreePop() 455 | 456 | lod_count = mesh.getNumLodLevels() 457 | if lod_count > 1 and ImGui.CollapsingHeader("LOD levels"): 458 | if self.lod_idx_override > -1: 459 | entity.setMeshLodBias(1, self.lod_idx_override, self.lod_idx_override) 460 | else: 461 | entity.setMeshLodBias(1) # reset LOD override 462 | strategy = mesh.getLodStrategy().getName() 463 | curr_idx = entity.getCurrentLodIndex() 464 | ImGui.AlignTextToFramePadding() 465 | ImGui.Text(f"Strategy: {strategy}") 466 | ImGui.SameLine() 467 | 468 | if ImGui.Checkbox("active", self.lod_idx_override == -1)[1]: 469 | self.lod_idx_override = -1 470 | elif self.lod_idx_override == -1: 471 | self.lod_idx_override = curr_idx 472 | 473 | for i in range(lod_count): 474 | txt = "Base Mesh" if i == 0 else f"Level {i}: {mesh.getLodLevel(i).userValue:.2f}" 475 | ImGui.Bullet() 476 | if ImGui.Selectable(txt, i == curr_idx): 477 | self.lod_idx_override = i 478 | 479 | if ImGui.IsItemHovered(): 480 | # force this LOD level 481 | entity.setMeshLodBias(1, i, i) 482 | 483 | if ImGui.CollapsingHeader("Bounds"): 484 | bounds = mesh.getBounds() 485 | 486 | if ImGui.BeginTable("Bounds", 4, ImGui.TableFlags_SizingStretchProp): 487 | s = bounds.getSize() 488 | draw_lbl_table_row("Size", s) 489 | c = bounds.getCenter() 490 | draw_lbl_table_row("Center", c) 491 | draw_lbl_table_row("Radius", (mesh.getBoundingSphereRadius(), None, None), ("{0:.2f}", None, None), ((1, 1, 1, 1), None, None)) 492 | ImGui.EndTable() 493 | 494 | if self.app.attach_node and ImGui.CollapsingHeader("Transform"): 495 | enode = entity.getParentSceneNode() 496 | 497 | if ImGui.BeginTable("Transform", 4, ImGui.TableFlags_SizingStretchProp): 498 | p = enode._getDerivedPosition() 499 | draw_lbl_table_row("Position", p) 500 | q = enode._getDerivedOrientation() 501 | o = [q.getPitch().valueDegrees(), q.getYaw().valueDegrees(), q.getRoll().valueDegrees()] 502 | draw_lbl_table_row("Orientation", o) 503 | s = enode._getDerivedScale() 504 | draw_lbl_table_row("Scale", s) 505 | ImGui.EndTable() 506 | 507 | ImGui.End() 508 | 509 | class MeshViewer(OgreBites.ApplicationContext, OgreBites.InputListener): 510 | 511 | def __init__(self, infile, rescfg): 512 | OgreBites.ApplicationContext.__init__(self, "OgreMeshViewer") 513 | OgreBites.InputListener.__init__(self) 514 | 515 | self.infile = infile 516 | if not self.infile: 517 | self.infile = askopenfilename() 518 | if not self.infile: 519 | raise SystemExit("No file selected") 520 | 521 | self.filename = None 522 | self.filedir = None 523 | self.rescfg = rescfg 524 | 525 | self.entity = None 526 | self.attach_node = None 527 | self.highlight_mat = None 528 | self.restart = False 529 | self.axes_visible = False 530 | self.fixed_yaw_axis = 1 531 | self.default_tilt = Ogre.Degree(20) 532 | self.grid_floor = None 533 | self.grid_visible = True 534 | 535 | self.active_controllers = {} 536 | 537 | self.next_rendersystem = "" 538 | self.next_campose = None 539 | 540 | # in case we want to show the file dialog 541 | root = tk.Tk() 542 | root.withdraw() 543 | 544 | def keyPressed(self, evt): 545 | if evt.keysym.sym == OgreBites.SDLK_ESCAPE: 546 | self.getRoot().queueEndRendering() 547 | if evt.keysym.sym == OgreBites.SDLK_KP_5: 548 | self._toggle_projection() 549 | elif evt.keysym.sym == ord("b"): 550 | self._toggle_bbox() 551 | elif evt.keysym.sym == ord("n"): 552 | self.gui.side_panel_visible = not self.gui.side_panel_visible 553 | elif evt.keysym.sym == ord("a"): 554 | self._toggle_axes() 555 | elif evt.keysym.sym == ord("g"): 556 | self._toggle_grid() 557 | elif evt.keysym.sym == ord("p"): 558 | self._save_screenshot() 559 | elif evt.keysym.sym == ord("w"): 560 | self._toggle_wireframe_mode() 561 | elif evt.keysym.sym == OgreBites.SDLK_F1: 562 | self.gui.load_file() 563 | elif evt.keysym.sym == OgreBites.SDLK_F5: 564 | self.reload(keep_cam=True) 565 | 566 | return True 567 | 568 | def mousePressed(self, evt): 569 | vp = self.cam.getViewport() 570 | ray = self.cam.getCameraToViewportRay(evt.x / vp.getActualWidth(), evt.y / vp.getActualHeight()) 571 | self.ray_query.setRay(ray) 572 | self.ray_query.setSortByDistance(True) 573 | for hit in self.ray_query.execute(): 574 | if evt.clicks == 2: 575 | self.camman.setPivotOffset(ray.getPoint(hit.distance)) 576 | return True 577 | 578 | new_entity = hit.movable.castEntity() 579 | 580 | if self.attach_node and new_entity and evt.button == OgreBites.BUTTON_LEFT: 581 | if self.entity is not None: 582 | self.entity.getParentSceneNode().showBoundingBox(False) 583 | 584 | self.entity = new_entity 585 | self.entity.getParentSceneNode().showBoundingBox(True) 586 | break 587 | 588 | return True 589 | 590 | def mouseWheelRolled(self, evt): 591 | if self.cam.getProjectionType() == Ogre.PT_ORTHOGRAPHIC: 592 | camnode = self.camman.getCamera() 593 | diam = camnode.getPosition().length() 594 | self.cam.setOrthoWindowHeight(diam) 595 | 596 | return True 597 | 598 | def _toggle_bbox(self): 599 | enode = self.entity.getParentSceneNode() 600 | enode.showBoundingBox(not enode.getShowBoundingBox()) 601 | 602 | def _toggle_wireframe_mode(self): 603 | polygon_mode = self.cam.getPolygonMode() 604 | 605 | if polygon_mode == Ogre.PM_SOLID: 606 | self.cam.setPolygonMode(Ogre.PM_WIREFRAME) 607 | if polygon_mode == Ogre.PM_WIREFRAME: 608 | self.cam.setPolygonMode(Ogre.PM_SOLID) 609 | 610 | def _toggle_axes(self): 611 | if not self.axes_visible: 612 | self.scn_mgr.addListener(self.axes) 613 | else: 614 | self.scn_mgr.removeListener(self.axes) 615 | 616 | self.axes_visible = not self.axes_visible 617 | 618 | def _toggle_grid(self): 619 | self.grid_visible = not self.grid_visible 620 | 621 | if self.grid_visible: 622 | self.grid_floor.show_plane(self.fixed_yaw_axis) 623 | else: 624 | self.grid_floor.show_plane(-1) 625 | 626 | def _toggle_projection(self): 627 | if self.cam.getProjectionType() == Ogre.PT_PERSPECTIVE: 628 | self.cam.setProjectionType(Ogre.PT_ORTHOGRAPHIC) 629 | camnode = self.camman.getCamera() 630 | diam = camnode.getPosition().length() 631 | self.cam.setOrthoWindowHeight(diam) 632 | else: 633 | self.cam.setProjectionType(Ogre.PT_PERSPECTIVE) 634 | 635 | def _save_screenshot(self): 636 | name = os.path.splitext(self.filename)[0] 637 | outpath = os.path.join(self.filedir, f"screenshot_{name}_") 638 | 639 | Ogre.LogManager.getSingleton().logMessage(f"Screenshot saved to folder: {os.path.normpath(self.filedir)}") 640 | 641 | self.cam.getViewport().setOverlaysEnabled(False) 642 | self.getRenderWindow().update(False) 643 | self.getRenderWindow().writeContentsToTimestampedFile(outpath, ".png") 644 | self.cam.getViewport().setOverlaysEnabled(True) 645 | 646 | def update_fixed_camera_yaw(self): 647 | camnode = self.camman.getCamera() 648 | diam = camnode.getPosition().length() 649 | camnode.setOrientation(Ogre.Quaternion.IDENTITY) 650 | 651 | yaw_axis = [0, 0, 0] 652 | yaw_axis[self.fixed_yaw_axis] = 1 653 | camnode.setFixedYawAxis(self.fixed_yaw_axis != -1, yaw_axis) 654 | self.camman.setFixedYaw(self.fixed_yaw_axis != -1) 655 | if self.grid_visible: 656 | self.grid_floor.show_plane(self.fixed_yaw_axis) 657 | 658 | if self.next_campose: 659 | camnode.setPosition(self.next_campose[0]) 660 | camnode.setOrientation(self.next_campose[1]) 661 | self.next_campose = None 662 | elif self.fixed_yaw_axis == 0: 663 | self.camman.setYawPitchDist(0, 0, diam) 664 | camnode.roll(-Ogre.Degree(90)) 665 | elif self.fixed_yaw_axis == 2: 666 | self.camman.setYawPitchDist(0, self.default_tilt - Ogre.Degree(90), diam) 667 | else: 668 | self.camman.setYawPitchDist(0, self.default_tilt, diam) 669 | 670 | def reload(self, keep_cam=False): 671 | if not app.infile: 672 | return 673 | 674 | if keep_cam: 675 | camnode = self.camman.getCamera() 676 | # multiply to store a copy instead of a reference 677 | self.next_campose = (camnode.getPosition()*1, camnode.getOrientation()*1) 678 | 679 | app.restart = True 680 | app.getRoot().queueEndRendering() 681 | 682 | def locateResources(self): 683 | self.filename = os.path.basename(self.infile) 684 | self.filedir = os.path.dirname(self.infile) 685 | 686 | rgm = Ogre.ResourceGroupManager.getSingleton() 687 | # ensure our resource group is separate, even with a local resources.cfg 688 | rgm.createResourceGroup(RGN_MESHVIEWER, False) 689 | 690 | # use parent implementation to locate system-wide RTShaderLib 691 | OgreBites.ApplicationContext.locateResources(self) 692 | 693 | if self.rescfg: 694 | cfg = Ogre.ConfigFile() 695 | cfg.loadDirect(self.rescfg) 696 | 697 | for sec, settings in cfg.getSettingsBySection().items(): 698 | for kind, loc in settings.items(): 699 | rgm.addResourceLocation(loc, kind, sec) 700 | 701 | # explicitly add mesh location to be safe 702 | if not rgm.resourceLocationExists(self.filedir, RGN_USERDATA): 703 | rgm.addResourceLocation(self.filedir, "FileSystem", RGN_USERDATA) 704 | 705 | # add fonts to default resource group 706 | rgm.addResourceLocation(os.path.dirname(__file__) + "/fonts", "FileSystem", RGN_MESHVIEWER) 707 | 708 | def loadResources(self): 709 | rgm = Ogre.ResourceGroupManager.getSingleton() 710 | rgm.initialiseResourceGroup(Ogre.RGN_INTERNAL) 711 | rgm.initialiseResourceGroup(RGN_MESHVIEWER) 712 | 713 | # only capture default group 714 | self.logwin = LogWindow() 715 | Ogre.LogManager.getSingleton().getDefaultLog().addListener(self.logwin) 716 | rgm.initialiseResourceGroup(RGN_USERDATA) 717 | 718 | rgm.setWorldResourceGroupName(RGN_USERDATA) # used by .scene loader 719 | 720 | def setup(self): 721 | if self.next_rendersystem: 722 | self.getRoot().setRenderSystem(self.getRoot().getRenderSystemByName(self.next_rendersystem)) 723 | 724 | OgreBites.ApplicationContext.setup(self) 725 | 726 | self.restart = False 727 | imgui_overlay = self.initialiseImGui() 728 | ImGui.GetIO().IniFilename = self.getFSLayer().getWritablePath("imgui.ini") 729 | 730 | root = self.getRoot() 731 | scn_mgr = root.createSceneManager() 732 | scn_mgr.addRenderQueueListener(self.getOverlaySystem()) 733 | self.scn_mgr = scn_mgr 734 | 735 | # set listener to deal with missing materials 736 | self.mat_creator = MaterialCreator() 737 | Ogre.MeshManager.getSingleton().setListener(self.mat_creator) 738 | 739 | # HiDPI 740 | pixel_ratio = self.getDisplayDPI() / 96 741 | Ogre.Overlay.OverlayManager.getSingleton().setPixelRatio(pixel_ratio) 742 | ImGui.GetStyle().ScaleAllSizes(pixel_ratio) 743 | 744 | # for picking 745 | self.ray_query = scn_mgr.createRayQuery(Ogre.Ray()) 746 | 747 | imgui_overlay.addFont("UIText", RGN_MESHVIEWER) 748 | self.logwin.font = imgui_overlay.addFont("LogText", RGN_MESHVIEWER) 749 | 750 | imgui_overlay.show() 751 | 752 | shadergen = OgreRTShader.ShaderGenerator.getSingleton() 753 | shadergen.addSceneManager(scn_mgr) # must be done before we do anything with the scene 754 | 755 | scn_mgr.setAmbientLight((.1, .1, .1)) 756 | 757 | self.highlight_mat = Ogre.MaterialManager.getSingleton().create("Highlight", RGN_MESHVIEWER) 758 | self.highlight_mat.getTechniques()[0].getPasses()[0].setEmissive((1, 1, 0)) 759 | 760 | main_cam_name = "MeshViewer/Cam" 761 | self.cam = scn_mgr.createCamera(main_cam_name) 762 | self.cam.setAutoAspectRatio(True) 763 | camnode = scn_mgr.getRootSceneNode().createChildSceneNode() 764 | camnode.attachObject(self.cam) 765 | 766 | vp = self.getRenderWindow().addViewport(self.cam) 767 | vp.setBackgroundColour((.3, .3, .3)) 768 | 769 | self.gui = MeshViewerGui(self) 770 | self.getRenderWindow().addListener(self.gui) 771 | 772 | # imgui needs warmup to render on first frame 773 | # see https://github.com/ocornut/imgui/issues/1893#issuecomment-399102821 774 | self.getRenderWindow().update(False) 775 | self.getRoot().renderOneFrame() 776 | 777 | Ogre.LogManager.getSingleton().logMessage(f"Opening file: {os.path.normpath(self.infile)}") 778 | 779 | if self.filename.lower().endswith(".scene"): 780 | self.attach_node = scn_mgr.getRootSceneNode().createChildSceneNode() 781 | self.attach_node.loadChildren(self.filename) 782 | 783 | self.attach_node._update(True, False) 784 | diam = self.attach_node._getWorldAABB().getSize().length() 785 | 786 | for c in scn_mgr.getCameras().values(): 787 | if c.getName() == main_cam_name: 788 | continue 789 | # the camera frustum of any contained camera blows the above heuristic 790 | # so use the camera position instead 791 | diam = c.getDerivedPosition().length() 792 | break 793 | else: 794 | self.attach_node = None 795 | self.entity = scn_mgr.createEntity(self.filename) 796 | scn_mgr.getRootSceneNode().createChildSceneNode().attachObject(self.entity) 797 | diam = self.entity.getBoundingBox().getSize().length() 798 | 799 | self.cam.setNearClipDistance(diam * 0.01) 800 | 801 | self.axes = Ogre.DefaultDebugDrawer() 802 | self.axes.setStatic(True) 803 | self.axes.drawAxes(Ogre.Affine3.IDENTITY, diam / 4) 804 | self.axes_visible = False 805 | 806 | if len(scn_mgr.getMovableObjects("Light")) == 0: 807 | # skip creating light, if scene already contains one 808 | light = scn_mgr.createLight("MainLight") 809 | light.setType(Ogre.Light.LT_DIRECTIONAL) 810 | light.setSpecularColour(Ogre.ColourValue.White) 811 | camnode.attachObject(light) 812 | 813 | self.grid_floor = GridFloor(diam, scn_mgr.getRootSceneNode()) 814 | 815 | self.camman = OgreBites.CameraMan(camnode) 816 | self.camman.setStyle(OgreBites.CS_ORBIT) 817 | 818 | # We need to set YawPitchDist to initial values, so "diam" is properly set 819 | self.camman.setYawPitchDist(0, self.default_tilt, diam) 820 | self.update_fixed_camera_yaw() 821 | 822 | self.input_dispatcher = OgreBites.InputListenerChain([self.getImGuiInputListener(), self.camman, self]) 823 | self.addInputListener(self.input_dispatcher) 824 | 825 | def windowResized(self, win): 826 | # remember the resolution for next start 827 | self.getRoot().getRenderSystem().setConfigOption("Video Mode", f"{win.getWidth()} x {win.getHeight()}") 828 | 829 | def shutdown(self): 830 | self.scn_mgr.removeListener(self.axes) 831 | Ogre.LogManager.getSingleton().getDefaultLog().removeListener(self.logwin) 832 | OgreBites.ApplicationContext.shutdown(self) 833 | 834 | self.entity = None 835 | self.axes = None 836 | 837 | 838 | if __name__ == "__main__": 839 | import argparse 840 | 841 | parser = argparse.ArgumentParser(description="Ogre Mesh Viewer") 842 | parser.add_argument("infile", nargs="?", help="path to a ogre .mesh, ogre .scene or any format supported by assimp") 843 | parser.add_argument("-c", "--rescfg", help="path to the resources.cfg") 844 | args = parser.parse_args() 845 | app = MeshViewer(args.infile, args.rescfg) 846 | 847 | while True: # allow auto restart 848 | try: 849 | app.initApp() 850 | app.getRoot().startRendering() 851 | app.closeApp() 852 | except RuntimeError as e: 853 | raise SystemExit(e) from e 854 | 855 | if not app.restart: break 856 | --------------------------------------------------------------------------------