├── 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 | 
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 | [](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 |
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 |
--------------------------------------------------------------------------------