Browse Source

Merge pull request #58 from Hoikas/progress

Improved export progress logging
pull/66/head
Adam Johnson 7 years ago committed by GitHub
parent
commit
7243e1bbcd
  1. 23
      korlib/texture.cpp
  2. 2
      korman/__init__.py
  3. 79
      korman/exporter/convert.py
  4. 60
      korman/exporter/etlight.py
  5. 304
      korman/exporter/logger.py
  6. 50
      korman/exporter/material.py
  7. 20
      korman/exporter/mesh.py
  8. 37
      korman/exporter/rtlight.py
  9. 1
      korman/korlib/__init__.py
  10. 83
      korman/korlib/console.py
  11. 8
      korman/korlib/texture.py
  12. 6
      korman/nodes/node_conditions.py
  13. 13
      korman/operators/op_export.py
  14. 4
      korman/properties/modifiers/render.py
  15. 2
      korman/properties/modifiers/sound.py

23
korlib/texture.cpp

@ -266,14 +266,19 @@ static int _generate_detail_map(pyGLTexture* self, uint8_t* buf, size_t bufsz, G
return 0; return 0;
} }
static _LevelData _get_level_data(pyGLTexture* self, GLint level, bool bgra, bool quiet) { static _LevelData _get_level_data(pyGLTexture* self, GLint level, bool bgra, PyObject* report) {
GLint width, height; GLint width, height;
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width); glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height); glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height);
GLenum fmt = bgra ? GL_BGRA_EXT : GL_RGBA; GLenum fmt = bgra ? GL_BGRA_EXT : GL_RGBA;
if (!quiet) // Print out the debug message
PySys_WriteStdout(" Level #%i: %ix%i\n", level, width, height); if (report && report != Py_None) {
PyObjectRef msg_func = PyObject_GetAttrString(report, "msg");
PyObjectRef args = Py_BuildValue("siii", "Level #{}: {}x{}", level, width, height);
PyObjectRef kwargs = Py_BuildValue("{s:i}", "indent", 2);
PyObjectRef result = PyObject_Call(msg_func, args, kwargs);
}
size_t bufsz; size_t bufsz;
bufsz = (width * height * 4); bufsz = (width * height * 4);
@ -284,18 +289,18 @@ static _LevelData _get_level_data(pyGLTexture* self, GLint level, bool bgra, boo
static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, PyObject* kwargs) { static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, PyObject* kwargs) {
static char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("bgra"), static char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("bgra"),
_pycs("quiet"), _pycs("fast"), NULL }; _pycs("report"), _pycs("fast"), NULL };
GLint level = 0; GLint level = 0;
bool calc_alpha = false; bool calc_alpha = false;
bool bgra = false; bool bgra = false;
bool quiet = false; PyObject* report = nullptr;
bool fast = false; bool fast = false;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ibbbb", kwlist, &level, &calc_alpha, &bgra, &quiet, &fast)) { if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ibbOb", kwlist, &level, &calc_alpha, &bgra, &report, &fast)) {
PyErr_SetString(PyExc_TypeError, "get_level_data expects an optional int, bool, bool, bool, bool"); PyErr_SetString(PyExc_TypeError, "get_level_data expects an optional int, bool, bool, obejct, bool");
return NULL; return NULL;
} }
_LevelData data = _get_level_data(self, level, bgra, quiet); _LevelData data = _get_level_data(self, level, bgra, report);
if (fast) if (fast)
return pyBuffer_Steal(data.m_data, data.m_dataSize); return pyBuffer_Steal(data.m_data, data.m_dataSize);
@ -372,7 +377,7 @@ static PyMethodDef pyGLTexture_Methods[] = {
}; };
static PyObject* pyGLTexture_get_has_alpha(pyGLTexture* self, void*) { static PyObject* pyGLTexture_get_has_alpha(pyGLTexture* self, void*) {
_LevelData data = _get_level_data(self, 0, false, true); _LevelData data = _get_level_data(self, 0, false, nullptr);
for (size_t i = 3; i < data.m_dataSize; i += 4) { for (size_t i = 3; i < data.m_dataSize; i += 4) {
if (data.m_data[i] != 255) { if (data.m_data[i] != 255) {
delete[] data.m_data; delete[] data.m_data;

2
korman/__init__.py

@ -22,7 +22,7 @@ from . import operators
bl_info = { bl_info = {
"name": "Korman", "name": "Korman",
"author": "Guild of Writers", "author": "Guild of Writers",
"blender": (2, 77, 0), "blender": (2, 78, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "Exporter for Cyan Worlds' Plasma Engine", "description": "Exporter for Cyan Worlds' Plasma Engine",
"warning": "beta", "warning": "beta",

79
korman/exporter/convert.py

@ -14,6 +14,7 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
from ..korlib import ConsoleToggler
from pathlib import Path from pathlib import Path
from PyHSPlasma import * from PyHSPlasma import *
import time import time
@ -42,19 +43,28 @@ class Exporter:
return Path(self._op.filepath).stem return Path(self._op.filepath).stem
def run(self): def run(self):
with logger.ExportLogger(self._op.filepath) as _log: log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger
print("Exporting '{}.age'".format(self.age_name)) with ConsoleToggler(self._op.show_console), log(self._op.filepath) as self.report:
start = time.perf_counter()
# Step 0: Init export resmgr and stuff # Step 0: Init export resmgr and stuff
self.mgr = manager.ExportManager(self) self.mgr = manager.ExportManager(self)
self.mesh = mesh.MeshConverter(self) self.mesh = mesh.MeshConverter(self)
self.report = logger.ExportAnalysis()
self.physics = physics.PhysicsConverter(self) self.physics = physics.PhysicsConverter(self)
self.light = rtlight.LightConverter(self) self.light = rtlight.LightConverter(self)
self.animation = animation.AnimationConverter(self) self.animation = animation.AnimationConverter(self)
self.sumfile = sumfile.SumFile() self.sumfile = sumfile.SumFile()
# Step 0.9: Init the progress mgr
self.report.progress_add_step("Collecting Objects")
self.report.progress_add_step("Harvesting Actors")
if self._op.bake_lighting:
etlight.LightBaker.add_progress_steps(self.report)
self.report.progress_add_step("Exporting Scene Objects")
self.report.progress_add_step("Exporting Logic Nodes")
self.report.progress_add_step("Finalizing Plasma Logic")
self.report.progress_add_step("Exporting Textures")
self.report.progress_add_step("Composing Geometry")
self.report.progress_start("EXPORTING AGE")
# Step 1: Create the age info and the pages # Step 1: Create the age info and the pages
self._export_age_info() self._export_age_info()
@ -91,17 +101,18 @@ class Exporter:
# Step 5.1: Save out the export report. # Step 5.1: Save out the export report.
# If the export fails and this doesn't save, we have bigger problems than # If the export fails and this doesn't save, we have bigger problems than
# these little warnings and notices. # these little warnings and notices.
self.report.progress_end()
self.report.save() self.report.save()
# And finally we crow about how awesomely fast we are...
end = time.perf_counter()
print("\nExported {}.age in {:.2f} seconds".format(self.age_name, end-start))
def _bake_static_lighting(self): def _bake_static_lighting(self):
oven = etlight.LightBaker() oven = etlight.LightBaker(self.report)
oven.bake_static_lighting(self._objects) oven.bake_static_lighting(self._objects)
def _collect_objects(self): def _collect_objects(self):
self.report.progress_advance()
self.report.progress_range = len(bpy.data.objects)
inc_progress = self.report.progress_increment
# Grab a naive listing of enabled pages # Grab a naive listing of enabled pages
age = bpy.context.scene.world.plasma_age age = bpy.context.scene.world.plasma_age
pages_enabled = frozenset([page.name for page in age.pages if page.enabled]) pages_enabled = frozenset([page.name for page in age.pages if page.enabled])
@ -136,6 +147,7 @@ class Exporter:
self._objects.append(obj) self._objects.append(obj)
elif page not in all_pages: elif page not in all_pages:
error.add(page, obj.name) error.add(page, obj.name)
inc_progress()
error.raise_if_error() error.raise_if_error()
def _export_age_info(self): def _export_age_info(self):
@ -163,7 +175,7 @@ class Exporter:
parent = bo.parent parent = bo.parent
if parent is not None: if parent is not None:
if parent.plasma_object.enabled: if parent.plasma_object.enabled:
print(" Attaching to parent SceneObject '{}'".format(parent.name)) self.report.msg("Attaching to parent SceneObject '{}'", parent.name, indent=1)
parent_ci = self._export_coordinate_interface(None, parent) parent_ci = self._export_coordinate_interface(None, parent)
parent_ci.addChild(so.key) parent_ci.addChild(so.key)
else: else:
@ -187,8 +199,13 @@ class Exporter:
return so.coord.object return so.coord.object
def _export_scene_objects(self): def _export_scene_objects(self):
self.report.progress_advance()
self.report.progress_range = len(self._objects)
inc_progress = self.report.progress_increment
log_msg = self.report.msg
for bl_obj in self._objects: for bl_obj in self._objects:
print("\n[SceneObject '{}']".format(bl_obj.name)) log_msg("\n[SceneObject '{}']".format(bl_obj.name))
# First pass: do things specific to this object type. # First pass: do things specific to this object type.
# note the function calls: to export a MESH, it's _export_mesh_blobj # note the function calls: to export a MESH, it's _export_mesh_blobj
@ -196,10 +213,10 @@ class Exporter:
try: try:
export_fn = getattr(self, export_fn) export_fn = getattr(self, export_fn)
except AttributeError: except AttributeError:
print("WARNING: '{}' is a Plasma Object of Blender type '{}'".format(bl_obj.name, bl_obj.type)) self.report.warn("""'{}' is a Plasma Object of Blender type '{}'
print("... And I have NO IDEA what to do with that! Tossing.") ... And I have NO IDEA what to do with that! Tossing.""".format(bl_obj.name, bl_obj.type))
continue continue
print(" Blender Object '{}' of type '{}'".format(bl_obj.name, bl_obj.type)) log_msg("Blender Object '{}' of type '{}'".format(bl_obj.name, bl_obj.type), indent=1)
# Create a sceneobject if one does not exist. # Create a sceneobject if one does not exist.
# Before we call the export_fn, we need to determine if this object is an actor of any # Before we call the export_fn, we need to determine if this object is an actor of any
@ -211,8 +228,9 @@ class Exporter:
# And now we puke out the modifiers... # And now we puke out the modifiers...
for mod in bl_obj.plasma_modifiers.modifiers: for mod in bl_obj.plasma_modifiers.modifiers:
print(" Exporting '{}' modifier as '{}'".format(mod.bl_label, mod.key_name)) log_msg("Exporting '{}' modifier".format(mod.bl_label), indent=1)
mod.export(self, bl_obj, sceneobject) mod.export(self, bl_obj, sceneobject)
inc_progress()
def _export_empty_blobj(self, so, bo): def _export_empty_blobj(self, so, bo):
# We don't need to do anything here. This function just makes sure we don't error out # We don't need to do anything here. This function just makes sure we don't error out
@ -227,21 +245,33 @@ class Exporter:
if bo.data.materials: if bo.data.materials:
self.mesh.export_object(bo) self.mesh.export_object(bo)
else: else:
print(" No material(s) on the ObData, so no drawables") self.report.msg("No material(s) on the ObData, so no drawables", indent=1)
def _export_referenced_node_trees(self): def _export_referenced_node_trees(self):
print("\nChecking Logic Trees...") self.report.progress_advance()
need_to_export = ((name, bo, so) for name, (bo, so) in self.want_node_trees.items() self.report.progress_range = len(self.want_node_trees)
if name not in self.node_trees_exported) inc_progress = self.report.progress_increment
self.report.msg("\nChecking Logic Trees...")
need_to_export = [(name, bo, so) for name, (bo, so) in self.want_node_trees.items()
if name not in self.node_trees_exported]
self.report.progress_value = len(self.want_node_trees) - len(need_to_export)
for tree, bo, so in need_to_export: for tree, bo, so in need_to_export:
print(" NodeTree '{}'".format(tree)) self.report.msg("NodeTree '{}'", tree, indent=1)
bpy.data.node_groups[tree].export(self, bo, so) bpy.data.node_groups[tree].export(self, bo, so)
inc_progress()
def _harvest_actors(self): def _harvest_actors(self):
self.report.progress_advance()
self.report.progress_range = len(self._objects) + len(bpy.data.textures)
inc_progress = self.report.progress_increment
for bl_obj in self._objects: for bl_obj in self._objects:
for mod in bl_obj.plasma_modifiers.modifiers: for mod in bl_obj.plasma_modifiers.modifiers:
if mod.enabled: if mod.enabled:
self.actors.update(mod.harvest_actors()) self.actors.update(mod.harvest_actors())
inc_progress()
# This is a little hacky, but it's an edge case... I guess? # This is a little hacky, but it's an edge case... I guess?
# We MUST have CoordinateInterfaces for EnvironmentMaps (DCMs, bah) # We MUST have CoordinateInterfaces for EnvironmentMaps (DCMs, bah)
@ -251,6 +281,7 @@ class Exporter:
viewpt = envmap.viewpoint_object viewpt = envmap.viewpoint_object
if viewpt is not None: if viewpt is not None:
self.actors.add(viewpt.name) self.actors.add(viewpt.name)
inc_progress()
def has_coordiface(self, bo): def has_coordiface(self, bo):
if bo.type in {"CAMERA", "EMPTY", "LAMP"}: if bo.type in {"CAMERA", "EMPTY", "LAMP"}:
@ -269,7 +300,9 @@ class Exporter:
return False return False
def _post_process_scene_objects(self): def _post_process_scene_objects(self):
print("\nPostprocessing SceneObjects...") self.report.progress_advance()
self.report.progress_range = len(self._objects)
inc_progress = self.report.progress_increment
mat_mgr = self.mesh.material mat_mgr = self.mesh.material
for bl_obj in self._objects: for bl_obj in self._objects:
@ -292,5 +325,5 @@ class Exporter:
for mod in bl_obj.plasma_modifiers.modifiers: for mod in bl_obj.plasma_modifiers.modifiers:
proc = getattr(mod, "post_export", None) proc = getattr(mod, "post_export", None)
if proc is not None: if proc is not None:
print(" '{}' modifier '{}'".format(bl_obj.name, mod.key_name))
proc(self, bl_obj, sceneobject) proc(self, bl_obj, sceneobject)
inc_progress()

60
korman/exporter/etlight.py

@ -16,6 +16,7 @@
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from .logger import ExportProgressLogger
from .mesh import _VERTEX_COLOR_LAYERS from .mesh import _VERTEX_COLOR_LAYERS
from ..helpers import * from ..helpers import *
@ -24,10 +25,27 @@ _NUM_RENDER_LAYERS = 20
class LightBaker: class LightBaker:
"""ExportTime Lighting""" """ExportTime Lighting"""
def __init__(self): def __init__(self, report=None):
self._lightgroups = {} self._lightgroups = {}
if report is None:
self._report = ExportProgressLogger()
self.add_progress_steps(self._report)
self._report.progress_start("PREVIEWING LIGHTING")
self._own_report = True
else:
self._report = report
self._own_report = False
self._uvtexs = {} self._uvtexs = {}
def __del__(self):
if self._own_report:
self._report.progress_end()
@staticmethod
def add_progress_steps(report):
report.progress_add_step("Searching for Bahro")
report.progress_add_step("Baking Static Lighting")
def _apply_render_settings(self, toggle, vcols): def _apply_render_settings(self, toggle, vcols):
render = bpy.context.scene.render render = bpy.context.scene.render
toggle.track(render, "use_textures", False) toggle.track(render, "use_textures", False)
@ -63,7 +81,7 @@ class LightBaker:
def bake_static_lighting(self, objs): def bake_static_lighting(self, objs):
"""Bakes all static lighting for Plasma geometry""" """Bakes all static lighting for Plasma geometry"""
print("\nBaking Static Lighting...") self._report.msg("\nBaking Static Lighting...")
bake = self._harvest_bakable_objects(objs) bake = self._harvest_bakable_objects(objs)
with GoodNeighbor() as toggle: with GoodNeighbor() as toggle:
@ -77,43 +95,51 @@ class LightBaker:
return result return result
def _bake_static_lighting(self, bake, toggle): def _bake_static_lighting(self, bake, toggle):
inc_progress = self._report.progress_increment
# Step 0.9: Make all layers visible. # Step 0.9: Make all layers visible.
# This prevents context operators from phailing. # This prevents context operators from phailing.
bpy.context.scene.layers = (True,) * _NUM_RENDER_LAYERS bpy.context.scene.layers = (True,) * _NUM_RENDER_LAYERS
# Step 1: Prepare... Apply UVs, etc, etc, etc # Step 1: Prepare... Apply UVs, etc, etc, etc
print(" Preparing to bake...") self._report.progress_advance()
self._report.progress_range = len(bake)
self._report.msg("Preparing to bake...", indent=1)
for key in bake.keys(): for key in bake.keys():
if key[0] == "lightmap": if key[0] == "lightmap":
for i in range(len(bake[key])-1, -1, -1): for i in range(len(bake[key])-1, -1, -1):
obj = bake[key][i] obj = bake[key][i]
if not self._prep_for_lightmap(obj, toggle): if not self._prep_for_lightmap(obj, toggle):
print(" Lightmap '{}' will not be baked -- no applicable lights".format(obj.name)) self._report.msg("Lightmap '{}' will not be baked -- no applicable lights",
obj.name, indent=2)
bake[key].pop(i) bake[key].pop(i)
elif key[0] == "vcol": elif key[0] == "vcol":
for i in range(len(bake[key])-1, -1, -1): for i in range(len(bake[key])-1, -1, -1):
obj = bake[key][i] obj = bake[key][i]
if not self._prep_for_vcols(obj, toggle): if not self._prep_for_vcols(obj, toggle):
if self._has_valid_material(obj): if self._has_valid_material(obj):
print(" VCols '{}' will not be baked -- no applicable lights".format(obj.name)) self._report.msg("VCols '{}' will not be baked -- no applicable lights",
obj.name, indent=2)
bake[key].pop(i) bake[key].pop(i)
else: else:
raise RuntimeError(key[0]) raise RuntimeError(key[0])
print(" ...") inc_progress()
self._report.msg(" ...")
# Step 2: BAKE! # Step 2: BAKE!
self._report.progress_advance()
self._report.progress_range = len(bake)
for key, value in bake.items(): for key, value in bake.items():
if not value: if value:
continue if key[0] == "lightmap":
self._report.msg("{} Lightmap(s) [H:{:X}]", len(value), hash(key), indent=1)
if key[0] == "lightmap": self._bake_lightmaps(value, key[1:])
print(" {} Lightmap(s) [H:{:X}]".format(len(value), hash(key))) elif key[0] == "vcol":
self._bake_lightmaps(value, key[1:]) self._report.msg("{} Crap Light(s)", len(value), indent=1)
elif key[0] == "vcol": self._bake_vcols(value)
print(" {} Crap Light(s)".format(len(value))) else:
self._bake_vcols(value) raise RuntimeError(key[0])
else: inc_progress()
raise RuntimeError(key[0])
# Return how many thingos we baked # Return how many thingos we baked
return sum(map(len, bake.values())) return sum(map(len, bake.values()))

304
korman/exporter/logger.py

@ -13,64 +13,282 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
from ..korlib import ConsoleToggler
from pathlib import Path from pathlib import Path
import sys import threading
import time
class ExportAnalysis: _HEADING_SIZE = 50
"""This is used to collect artist action items from the export process. You can warn about _MAX_ELIPSES = 3
portability issues, possible oversights, etc. The benefit here is that the user doesn't have _MAX_TIME_UNTIL_ELIPSES = 2.0
to look through all of the gobbledygook in the export log.
"""
_porting = [] class _ExportLogger:
_warnings = [] def __init__(self, print_logs, age_path=None):
self._porting = []
self._warnings = []
self._age_path = Path(age_path) if age_path is not None else None
self._file = None
self._print_logs = print_logs
self._time_start_overall = 0
def __enter__(self):
assert self._age_path is not None
# Make the log file name from the age file path -- this ensures we're not trying to write
# the log file to the same directory Blender.exe is in, which might be a permission error
my_path = self._age_path.with_name("{}_export".format(self._age_path.stem)).with_suffix(".log")
self._file = open(str(my_path), "w")
return self
def __exit__(self, type, value, traceback):
if value is not None:
ConsoleToggler().keep_console = True
self._file.close()
return False
def msg(self, *args, **kwargs):
assert args
if self._file is not None:
indent = kwargs.get("indent", 0)
msg = "{}{}".format(" " * indent, args[0])
if len(args) > 1:
msg = msg.format(*args[1:], **kwargs)
self._file.writelines((msg, "\n"))
if self._print_logs:
print(msg)
def port(self, *args, **kwargs):
assert args
indent = kwargs.get("indent", 0)
msg = "{}PORTING: {}".format(" " * indent, args[0])
if len(args) > 1:
msg = msg.format(*args[1:], **kwargs)
if self._file is not None:
self._file.writelines((msg, "\n"))
if self._print_logs:
print(msg)
self._porting.append(args[0])
def progress_add_step(self, name):
pass
def progress_advance(self):
pass
def progress_complete_step(self):
pass
def progress_end(self):
if self._age_path is not None:
export_time = time.perf_counter() - self._time_start_overall
self.msg("\nExported '{}' in {:.2f}s", self._age_path.name, export_time)
# Ensure the got dawg thread goes good-bye
self._thread.join(timeout=5.0)
assert not self._thread.is_alive()
def progress_increment(self):
pass
def progress_start(self, action):
if self._age_path is not None:
self.msg("Exporting '{}'", self._age_path.name)
self._time_start_overall = time.perf_counter()
def save(self): def save(self):
# TODO # TODO
pass pass
def port(self, message, indent=0): def warn(self, *args, **kwargs):
self._porting.append(message) assert args
print(" " * indent, end="") indent = kwargs.get("indent", 0)
print("PORTING: {}".format(message)) msg = "{}WARNING: {}".format(" " * indent, args[0])
if len(args) > 1:
msg = msg.format(*args[1:], **kwargs)
if self._file is not None:
self._file.writelines((msg, "\n"))
if self._print_logs:
print(msg)
self._warnings.append(args[0])
def warn(self, message, indent=0):
self._warnings.append(message)
print(" " * indent, end="")
print("WARNING: {}".format(message))
class ExportProgressLogger(_ExportLogger):
def __init__(self, age_path=None):
super().__init__(False, age_path)
class ExportLogger: # Long running operations like the Blender bake_image call make it seem like we've hung
"""Yet Another Logger(TM)""" # because it is difficult to inspect the progress of Blender's internal operators. The best
# solution here is to move printing into a thread that can detect long-running ops and display
# something visible such as a moving elipsis
self._thread = threading.Thread(target=self._progress_thread)
self._queued_lines = []
self._print_condition = threading.Condition()
self._volatile_line = ""
def __init__(self, ageFile): # Progress manager
# Make the log file name from the age file path -- this ensures we're not trying to write self._progress_alive = False
# the log file to the same directory Blender.exe is in, which might be a permission error self._progress_steps = []
my_path = Path(ageFile) self._step_id = -1
my_path = my_path.with_name("{}_export".format(my_path.stem)).with_suffix(".log") self._step_max = 0
self._file = open(str(my_path), "w") self._step_progress = 0
self._time_start_step = 0
for i in dir(self._file): def __exit__(self, type, value, traceback):
if not hasattr(self, i): if value is not None:
setattr(self, i, getattr(self._file, i)) export_time = time.perf_counter() - self._time_start_overall
with self._print_condition:
self._progress_print_step(done=(self._step_progress == self._step_max), error=True)
self._progress_print_line("\nABORTED AFTER {:.2f}s".format(export_time))
self._progress_print_heading("ERROR")
self._progress_print_line(str(value))
self._progress_print_heading()
self._progress_alive = False
return super().__exit__(type, value, traceback)
def __enter__(self): def progress_add_step(self, name):
self._stdout, sys.stdout = sys.stdout, self._file assert self._step_id == -1
self._stderr, sys.stderr = sys.stderr, self._file self._progress_steps.append(name)
def __exit__(self, type, value, traceback): def progress_advance(self):
sys.stdout = self._stdout """Advances the progress bar to the next step"""
sys.stderr = self._stderr if self._step_id != -1:
self._progress_print_step(done=True)
assert self._step_id < len(self._progress_steps)
self._step_id += 1
self._step_max = 0
self._step_progress = 0
self._time_start_step = time.perf_counter()
self._progress_print_step()
def progress_complete_step(self):
"""Manually completes the current step"""
assert self._step_id != -1
self._progress_print_step(done=True)
def progress_end(self):
self._progress_print_step(done=True)
assert self._step_id+1 == len(self._progress_steps)
export_time = time.perf_counter() - self._time_start_overall
with self._print_condition:
if self._age_path is not None:
self.msg("\nExported '{}' in {:.2f}s", self._age_path.name, export_time)
self._progress_print_line("\nEXPORTED '{}' IN {:.2f}s".format(self._age_path.name, export_time))
else:
self._progress_print_line("\nCOMPLETED IN {:.2f}s".format(export_time))
self._progress_print_heading()
self._progress_print_line("")
self._progress_alive = False
# Ensure the got dawg thread goes good-bye
self._thread.join(timeout=5.0)
assert not self._thread.is_alive()
def progress_increment(self):
"""Increments the progress of the current step"""
assert self._step_id != -1
self._step_progress += 1
if self._step_max != 0:
self._progress_print_step()
def _progress_print_line(self, line):
with self._print_condition:
self._queued_lines.append(line)
self._print_condition.notify()
def _progress_print_volatile(self, line):
with self._print_condition:
self._volatile_line = line
self._print_condition.notify()
def flush(self): def _progress_print_heading(self, text=None):
self._file.flush() if text:
self._stdout.flush() num_chars = len(text)
self._stderr.flush() border = "-" * int((_HEADING_SIZE - (num_chars + 2)) / 2)
pad = " " if num_chars % 2 == 1 else ""
line = "{border} {pad}{text} {border}".format(border=border, pad=pad, text=text)
self._progress_print_line(line)
else:
self._progress_print_line("-" * _HEADING_SIZE)
def write(self, str): def _progress_print_step(self, done=False, error=False):
self._file.write(str) with self._print_condition:
self._stdout.write(str) if done:
stage = "DONE IN {:.2f}s".format(time.perf_counter() - self._time_start_step)
print_func = self._progress_print_line
self._progress_print_volatile("")
else:
if self._step_max != 0 and self._step_progress != 0:
stage = "{} of {}".format(self._step_progress, self._step_max)
else:
stage = ""
print_func = self._progress_print_line if error else self._progress_print_volatile
def writelines(self, seq): line = "{}\t(step {}/{}): {}".format(self._progress_steps[self._step_id], self._step_id+1,
self._file.writelines(seq) len(self._progress_steps), stage)
self._stdout.writelines(seq) print_func(line)
def _progress_get_max(self):
return self._step_max
def _progress_set_max(self, value):
assert self._step_id != -1
self._step_max = value
self._progress_print_step()
progress_range = property(_progress_get_max, _progress_set_max)
def progress_start(self, action):
super().progress_start(action)
self._progress_print_heading("Korman")
self._progress_print_heading(action)
self._progress_alive = True
self._thread.start()
def _progress_thread(self):
num_dots = 0
while self._progress_alive:
with self._print_condition:
signalled = self._print_condition.wait(timeout=1.0)
print(end='\r')
# First, we need to print out any queued whole lines.
# NOTE: no need to lock anything here as Blender uses CPython (GIL)
if self._queued_lines:
print(*self._queued_lines, sep='\n')
self._queued_lines.clear()
# Now, we need to print out the current volatile line, if any.
if self._volatile_line:
print(self._volatile_line, end="")
# If the proc is long running, let us display some elipses so as to not alarm the user
if self._time_start_step != 0:
if (time.perf_counter() - self._time_start_step) > _MAX_TIME_UNTIL_ELIPSES:
num_dots = 0 if signalled or num_dots == _MAX_ELIPSES else num_dots + 1
else:
num_dots = 0
print('.' * num_dots, end=" " * (_MAX_ELIPSES - num_dots))
def _progress_get_current(self):
return self._step_progress
def _progress_set_current(self, value):
assert self._step_id != -1
self._step_progress = value
if self._step_max != 0:
self._progress_print_step()
progress_value = property(_progress_get_current, _progress_set_current)
class ExportVerboseLogger(_ExportLogger):
def __init__(self, age_path):
super().__init__(True, age_path)
self.progress_range = 0
self.progress_value = 0
def __exit__(self, type, value, traceback):
if value is not None:
export_time = time.perf_counter() - self._time_start_overall
self.msg("\nAborted after {:.2f}s", export_time)
self.msg("Error: {}", value)
return super().__exit__(type, value, traceback)

50
korman/exporter/material.py

@ -125,7 +125,7 @@ class MaterialConverter:
def export_material(self, bo, bm): def export_material(self, bo, bm):
"""Exports a Blender Material as an hsGMaterial""" """Exports a Blender Material as an hsGMaterial"""
print(" Exporting Material '{}'".format(bm.name)) self._report.msg("Exporting Material '{}'", bm.name, indent=1)
hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo) hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo)
slots = [(idx, slot) for idx, slot in enumerate(bm.texture_slots) if slot is not None and slot.use \ slots = [(idx, slot) for idx, slot in enumerate(bm.texture_slots) if slot is not None and slot.use \
@ -198,7 +198,7 @@ class MaterialConverter:
return hsgmat.key return hsgmat.key
def export_waveset_material(self, bo, bm): def export_waveset_material(self, bo, bm):
print(" Exporting WaveSet Material '{}'".format(bm.name)) self._report.msg("Exporting WaveSet Material '{}'", bm.name, indent=1)
# WaveSets MUST have their own material # WaveSets MUST have their own material
unique_name = "{}_WaveSet7".format(bm.name) unique_name = "{}_WaveSet7".format(bm.name)
@ -215,7 +215,7 @@ class MaterialConverter:
def export_bumpmap_slot(self, bo, bm, hsgmat, slot, idx): def export_bumpmap_slot(self, bo, bm, hsgmat, slot, idx):
name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name) name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name)
print(" Exporting Plasma Bumpmap Layers for '{}'".format(name)) self._report.msg("Exporting Plasma Bumpmap Layers for '{}'", name, indent=2)
# Okay, now we need to make 3 layers for the Du, Dw, and Dv # Okay, now we need to make 3 layers for the Du, Dw, and Dv
du_layer = self._mgr.add_object(plLayer, name="{}_DU_BumpLut".format(name), bl=bo) du_layer = self._mgr.add_object(plLayer, name="{}_DU_BumpLut".format(name), bl=bo)
@ -264,7 +264,7 @@ class MaterialConverter:
def export_texture_slot(self, bo, bm, hsgmat, slot, idx, name=None, blend_flags=True): def export_texture_slot(self, bo, bm, hsgmat, slot, idx, name=None, blend_flags=True):
if name is None: if name is None:
name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name) name = "{}_{}".format(bm.name if bm is not None else bo.name, slot.name)
print(" Exporting Plasma Layer '{}'".format(name)) self._report.msg("Exporting Plasma Layer '{}'", name, indent=2)
layer = self._mgr.add_object(plLayer, name=name, bl=bo) layer = self._mgr.add_object(plLayer, name=name, bl=bo)
if bm is not None and not slot.use_map_normal: if bm is not None and not slot.use_map_normal:
self._propagate_material_settings(bm, layer) self._propagate_material_settings(bm, layer)
@ -274,10 +274,10 @@ class MaterialConverter:
for i, uvchan in enumerate(bo.data.uv_layers): for i, uvchan in enumerate(bo.data.uv_layers):
if uvchan.name == slot.uv_layer: if uvchan.name == slot.uv_layer:
layer.UVWSrc = i layer.UVWSrc = i
print(" Using UV Map #{} '{}'".format(i, name)) self._report.msg("Using UV Map #{} '{}'", i, name, indent=3)
break break
else: else:
print(" No UVMap specified... Blindly using the first one, maybe it exists :|") self._report.msg("No UVMap specified... Blindly using the first one, maybe it exists :|", indent=3)
# Transform # Transform
xform = hsMatrix44() xform = hsMatrix44()
@ -447,7 +447,8 @@ class MaterialConverter:
name = "{}_DynEnvMap".format(viewpt.name) name = "{}_DynEnvMap".format(viewpt.name)
pl_env = self._mgr.find_object(pl_class, bl=bo, name=name) pl_env = self._mgr.find_object(pl_class, bl=bo, name=name)
if pl_env is not None: if pl_env is not None:
print(" EnvMap for viewpoint {} already exported... NOTE: Your settings here will be overridden by the previous object!".format(viewpt.name)) self._report.msg("EnvMap for viewpoint {} already exported... NOTE: Your settings here will be overridden by the previous object!",
viewpt.name, indent=3)
if isinstance(pl_env, plDynamicCamMap): if isinstance(pl_env, plDynamicCamMap):
pl_env.addTargetNode(self._mgr.find_key(plSceneObject, bl=bo)) pl_env.addTargetNode(self._mgr.find_key(plSceneObject, bl=bo))
pl_env.addMatLayer(layer.key) pl_env.addMatLayer(layer.key)
@ -457,7 +458,7 @@ class MaterialConverter:
oRes = bl_env.resolution oRes = bl_env.resolution
eRes = helpers.ensure_power_of_two(oRes) eRes = helpers.ensure_power_of_two(oRes)
if oRes != eRes: if oRes != eRes:
print(" Overriding EnvMap size to ({}x{}) -- POT".format(eRes, eRes)) self._report.msg("Overriding EnvMap size to ({}x{}) -- POT", eRes, eRes, indent=3)
# And now for the general ho'hum-ness # And now for the general ho'hum-ness
pl_env = self._mgr.add_object(pl_class, bl=bo, name=name) pl_env = self._mgr.add_object(pl_class, bl=bo, name=name)
@ -595,10 +596,11 @@ class MaterialConverter:
detail_fade_start=layer_props.detail_fade_start, detail_fade_stop=layer_props.detail_fade_stop, detail_fade_start=layer_props.detail_fade_start, detail_fade_stop=layer_props.detail_fade_stop,
detail_opacity_start=layer_props.detail_opacity_start, detail_opacity_stop=layer_props.detail_opacity_stop) detail_opacity_start=layer_props.detail_opacity_start, detail_opacity_stop=layer_props.detail_opacity_stop)
if key not in self._pending: if key not in self._pending:
print(" Stashing '{}' for conversion as '{}'".format(texture.image.name, str(key))) self._report.msg("Stashing '{}' for conversion as '{}'",
texture.image.name, str(key), indent=3)
self._pending[key] = [layer.key,] self._pending[key] = [layer.key,]
else: else:
print(" Found another user of '{}'".format(texture.image.name)) self._report.msg("Found another user of '{}'", texture.image.name, indent=3)
self._pending[key].append(layer.key) self._pending[key].append(layer.key)
def _export_texture_type_none(self, bo, layer, texture): def _export_texture_type_none(self, bo, layer, texture):
@ -609,16 +611,20 @@ class MaterialConverter:
"""This exports an externally prepared layer and image""" """This exports an externally prepared layer and image"""
key = _Texture(image=image) key = _Texture(image=image)
if key not in self._pending: if key not in self._pending:
print(" Stashing '{}' for conversion as '{}'".format(image.name, str(key))) self._report.msg("Stashing '{}' for conversion as '{}'", image.name, key, indent=2)
self._pending[key] = [layer.key,] self._pending[key] = [layer.key,]
else: else:
print(" Found another user of '{}'".format(key)) self._report.msg("Found another user of '{}'", key, indent=2)
self._pending[key].append(layer.key) self._pending[key].append(layer.key)
def finalize(self): def finalize(self):
self._report.progress_advance()
self._report.progress_range = len(self._pending)
inc_progress = self._report.progress_increment
for key, layers in self._pending.items(): for key, layers in self._pending.items():
name = str(key) name = str(key)
print("\n[Mipmap '{}']".format(name)) self._report.msg("\n[Mipmap '{}']", name)
image = key.image image = key.image
oWidth, oHeight = image.size oWidth, oHeight = image.size
@ -628,7 +634,8 @@ class MaterialConverter:
eWidth = helpers.ensure_power_of_two(oWidth) eWidth = helpers.ensure_power_of_two(oWidth)
eHeight = helpers.ensure_power_of_two(oHeight) eHeight = helpers.ensure_power_of_two(oHeight)
if (eWidth != oWidth) or (eHeight != oHeight): if (eWidth != oWidth) or (eHeight != oHeight):
print(" Image is not a POT ({}x{}) resizing to {}x{}".format(oWidth, oHeight, eWidth, eHeight)) self._report.msg("Image is not a POT ({}x{}) resizing to {}x{}",
oWidth, oHeight, eWidth, eHeight, indent=1)
self._resize_image(image, eWidth, eHeight) self._resize_image(image, eWidth, eHeight)
# Some basic mipmap settings. # Some basic mipmap settings.
@ -640,11 +647,11 @@ class MaterialConverter:
with helper as glimage: with helper as glimage:
if key.mipmap: if key.mipmap:
numLevels = glimage.num_levels numLevels = glimage.num_levels
print(" Generating mip levels") self._report.msg("Generating mip levels", indent=1)
glimage.generate_mipmap() glimage.generate_mipmap()
else: else:
numLevels = 1 numLevels = 1
print(" Stuffing image data") self._report.msg("Stuffing image data", indent=1)
# Uncompressed bitmaps are BGRA # Uncompressed bitmaps are BGRA
fmt = compression == plBitmap.kUncompressed fmt = compression == plBitmap.kUncompressed
@ -653,7 +660,7 @@ class MaterialConverter:
# this mipmap for per-page textures :( # this mipmap for per-page textures :(
data = [] data = []
for i in range(numLevels): for i in range(numLevels):
data.append(glimage.get_level_data(i, key.calc_alpha, fmt)) data.append(glimage.get_level_data(i, key.calc_alpha, fmt, report=self._report))
# Be a good citizen and reset the Blender Image to pre-futzing state # Be a good citizen and reset the Blender Image to pre-futzing state
image.reload() image.reload()
@ -663,9 +670,9 @@ class MaterialConverter:
mgr = self._mgr mgr = self._mgr
pages = {} pages = {}
print(" Adding to Layer(s)") self._report.msg("Adding to Layer(s)", indent=1)
for layer in layers: for layer in layers:
print(" {}".format(layer.name)) self._report.msg(layer.name, indent=2)
page = mgr.get_textures_page(layer) # Layer's page or Textures.prp page = mgr.get_textures_page(layer) # Layer's page or Textures.prp
# If we haven't created this plMipmap in the page (either layer's page or Textures.prp), # If we haven't created this plMipmap in the page (either layer's page or Textures.prp),
@ -680,6 +687,7 @@ class MaterialConverter:
else: else:
mipmap = pages[page] mipmap = pages[page]
layer.object.texture = mipmap.key layer.object.texture = mipmap.key
inc_progress()
def get_materials(self, bo): def get_materials(self, bo):
return self._obj2mat.get(bo, []) return self._obj2mat.get(bo, [])
@ -722,6 +730,10 @@ class MaterialConverter:
layer.runtime = utils.color(bm.diffuse_color) layer.runtime = utils.color(bm.diffuse_color)
layer.specular = utils.color(bm.specular_color) layer.specular = utils.color(bm.specular_color)
@property
def _report(self):
return self._exporter().report
def _resize_image(self, image, width, height): def _resize_image(self, image, width, height):
image.scale(width, height) image.scale(width, height)
image.update() image.update()

20
korman/exporter/mesh.py

@ -174,11 +174,15 @@ class MeshConverter:
def finalize(self): def finalize(self):
"""Prepares all baked Plasma geometry to be flushed to the disk""" """Prepares all baked Plasma geometry to be flushed to the disk"""
self._report.progress_advance()
self._report.progress_range = len(self._dspans)
inc_progress = self._report.progress_increment
log_msg = self._report.msg
log_msg("\nFinalizing Geometry")
for loc in self._dspans.values(): for loc in self._dspans.values():
for dspan in loc.values(): for dspan in loc.values():
print("\n[DrawableSpans '{}']".format(dspan.key.name)) log_msg("[DrawableSpans '{}']", dspan.key.name, indent=1)
print(" Composing geometry data")
# This mega-function does a lot: # This mega-function does a lot:
# 1. Converts SourceSpans (geospans) to Icicles and bakes geometry into plGBuffers # 1. Converts SourceSpans (geospans) to Icicles and bakes geometry into plGBuffers
@ -186,10 +190,7 @@ class MeshConverter:
# 3. Builds the plSpaceTree # 3. Builds the plSpaceTree
# 4. Clears the SourceSpans # 4. Clears the SourceSpans
dspan.composeGeometry(True, True) dspan.composeGeometry(True, True)
inc_progress()
# Might as well say something else just to fascinate anyone who is playing along
# at home (and actually enjoys reading these lawgs)
print(" Bounds and SpaceTree in the saddle")
def _export_geometry(self, bo, mesh, materials, geospans): def _export_geometry(self, bo, mesh, materials, geospans):
geodata = [_GeoData(len(mesh.vertices)) for i in materials] geodata = [_GeoData(len(mesh.vertices)) for i in materials]
@ -422,7 +423,8 @@ class MeshConverter:
_diindices = {} _diindices = {}
for geospan, pass_index in geospans: for geospan, pass_index in geospans:
dspan = self._find_create_dspan(bo, geospan.material.object, pass_index) dspan = self._find_create_dspan(bo, geospan.material.object, pass_index)
print(" Exported hsGMaterial '{}' geometry into '{}'".format(geospan.material.name, dspan.key.name)) self._report.msg("Exported hsGMaterial '{}' geometry into '{}'",
geospan.material.name, dspan.key.name, indent=1)
idx = dspan.addSourceSpan(geospan) idx = dspan.addSourceSpan(geospan)
if dspan not in _diindices: if dspan not in _diindices:
_diindices[dspan] = [idx,] _diindices[dspan] = [idx,]
@ -497,3 +499,7 @@ class MeshConverter:
@property @property
def _mgr(self): def _mgr(self):
return self._exporter().mgr return self._exporter().mgr
@property
def _report(self):
return self._exporter().report

37
korman/exporter/rtlight.py

@ -43,19 +43,19 @@ class LightConverter:
# If you change these calculations, be sure to update the AnimationConverter! # If you change these calculations, be sure to update the AnimationConverter!
intens, attenEnd = self.convert_attenuation(bl) intens, attenEnd = self.convert_attenuation(bl)
if bl.falloff_type == "CONSTANT": if bl.falloff_type == "CONSTANT":
print(" Attenuation: No Falloff") self._report.msg("Attenuation: No Falloff", indent=2)
pl.attenConst = intens pl.attenConst = intens
pl.attenLinear = 0.0 pl.attenLinear = 0.0
pl.attenQuadratic = 0.0 pl.attenQuadratic = 0.0
pl.attenCutoff = attenEnd pl.attenCutoff = attenEnd
elif bl.falloff_type == "INVERSE_LINEAR": elif bl.falloff_type == "INVERSE_LINEAR":
print(" Attenuation: Inverse Linear") self._report.msg("Attenuation: Inverse Linear", indent=2)
pl.attenConst = 1.0 pl.attenConst = 1.0
pl.attenLinear = self.convert_attenuation_linear(intens, attenEnd) pl.attenLinear = self.convert_attenuation_linear(intens, attenEnd)
pl.attenQuadratic = 0.0 pl.attenQuadratic = 0.0
pl.attenCutoff = attenEnd pl.attenCutoff = attenEnd
elif bl.falloff_type == "INVERSE_SQUARE": elif bl.falloff_type == "INVERSE_SQUARE":
print(" Attenuation: Inverse Square") self._report.msg("Attenuation: Inverse Square", indent=2)
pl.attenConst = 1.0 pl.attenConst = 1.0
pl.attenLinear = 0.0 pl.attenLinear = 0.0
pl.attenQuadratic = self.convert_attenuation_quadratic(intens, attenEnd) pl.attenQuadratic = self.convert_attenuation_quadratic(intens, attenEnd)
@ -75,18 +75,18 @@ class LightConverter:
return max(0.0, (intensity * _FAR_POWER - 1.0) / pow(end, 2)) return max(0.0, (intensity * _FAR_POWER - 1.0) / pow(end, 2))
def _convert_area_lamp(self, bl, pl): def _convert_area_lamp(self, bl, pl):
print(" [LimitedDirLightInfo '{}']".format(bl.name)) self._report.msg("[LimitedDirLightInfo '{}']", bl.name, indent=1)
pl.width = bl.size pl.width = bl.size
pl.depth = bl.size if bl.shape == "SQUARE" else bl.size_y pl.depth = bl.size if bl.shape == "SQUARE" else bl.size_y
pl.height = bl.plasma_lamp.size_height pl.height = bl.plasma_lamp.size_height
def _convert_point_lamp(self, bl, pl): def _convert_point_lamp(self, bl, pl):
print(" [OmniLightInfo '{}']".format(bl.name)) self._report.msg("[OmniLightInfo '{}']", bl.name, indent=1)
self._convert_attenuation(bl, pl) self._convert_attenuation(bl, pl)
def _convert_spot_lamp(self, bl, pl): def _convert_spot_lamp(self, bl, pl):
print(" [SpotLightInfo '{}']".format(bl.name)) self._report.msg("[SpotLightInfo '{}']", bl.name, indent=1)
self._convert_attenuation(bl, pl) self._convert_attenuation(bl, pl)
# Spot lights have a few more things... # Spot lights have a few more things...
@ -102,7 +102,7 @@ class LightConverter:
pl.falloff = 1.0 pl.falloff = 1.0
def _convert_sun_lamp(self, bl, pl): def _convert_sun_lamp(self, bl, pl):
print(" [DirectionalLightInfo '{}']".format(bl.name)) self._report.msg("[DirectionalLightInfo '{}']", bl.name, indent=1)
def export_rtlight(self, so, bo): def export_rtlight(self, so, bo):
bl_light = bo.data bl_light = bo.data
@ -132,18 +132,18 @@ class LightConverter:
# Apply the colors # Apply the colors
if bl_light.use_diffuse and not shadow_only: if bl_light.use_diffuse and not shadow_only:
print(" Diffuse: {}".format(diff_str)) self._report.msg("Diffuse: {}", diff_str, indent=2)
pl_light.diffuse = hsColorRGBA(*diff_color) pl_light.diffuse = hsColorRGBA(*diff_color)
else: else:
print(" Diffuse: OFF") self._report.msg("Diffuse: OFF", indent=2)
pl_light.diffuse = hsColorRGBA(0.0, 0.0, 0.0, energy) pl_light.diffuse = hsColorRGBA(0.0, 0.0, 0.0, energy)
if bl_light.use_specular and not shadow_only: if bl_light.use_specular and not shadow_only:
print(" Specular: {}".format(spec_str)) self._report.msg("Specular: {}", spec_str, indent=2)
pl_light.setProperty(plLightInfo.kLPHasSpecular, True) pl_light.setProperty(plLightInfo.kLPHasSpecular, True)
pl_light.specular = hsColorRGBA(*spec_color) pl_light.specular = hsColorRGBA(*spec_color)
else: else:
print(" Specular: OFF") self._report.msg("Specular: OFF", indent=2)
pl_light.specular = hsColorRGBA(0.0, 0.0, 0.0, energy) pl_light.specular = hsColorRGBA(0.0, 0.0, 0.0, energy)
rtlamp = bl_light.plasma_lamp rtlamp = bl_light.plasma_lamp
@ -202,7 +202,7 @@ class LightConverter:
# projection Lamp with our own faux Material. Unfortunately, Plasma only supports projecting # projection Lamp with our own faux Material. Unfortunately, Plasma only supports projecting
# one layer. We could exploit the fUnderLay and fOverLay system to export everything, but meh. # one layer. We could exploit the fUnderLay and fOverLay system to export everything, but meh.
if len(tex_slots) > 1: if len(tex_slots) > 1:
self._exporter().warn("Only one texture slot can be exported per Lamp. Picking the first one: '{}'".format(slot.name), indent=3) self._report.warn("Only one texture slot can be exported per Lamp. Picking the first one: '{}'".format(slot.name), indent=3)
layer = mat.export_texture_slot(bo, None, None, slot, 0, blend_flags=False) layer = mat.export_texture_slot(bo, None, None, slot, 0, blend_flags=False)
state = layer.state state = layer.state
@ -243,7 +243,7 @@ class LightConverter:
def find_material_light_keys(self, bo, bm): def find_material_light_keys(self, bo, bm):
"""Given a blender material, we find the keys of all matching Plasma RT Lights. """Given a blender material, we find the keys of all matching Plasma RT Lights.
NOTE: We return a tuple of lists: ([permaLights], [permaProjs])""" NOTE: We return a tuple of lists: ([permaLights], [permaProjs])"""
print(" Searching for runtime lights...") self._report.msg("Searching for runtime lights...", indent=1)
permaLights = [] permaLights = []
permaProjs = [] permaProjs = []
@ -272,16 +272,17 @@ class LightConverter:
break break
else: else:
# didn't find a layer where both lamp and object were, skip it. # didn't find a layer where both lamp and object were, skip it.
print(" [{}] '{}': not in same layer, skipping...".format(lamp.type, obj.name)) self._report.msg("[{}] '{}': not in same layer, skipping...",
lamp.type, obj.name, indent=2)
continue continue
# This is probably where PermaLight vs PermaProj should be sorted out... # This is probably where PermaLight vs PermaProj should be sorted out...
pl_light = self.get_light_key(obj, lamp, None) pl_light = self.get_light_key(obj, lamp, None)
if self._is_projection_lamp(lamp): if self._is_projection_lamp(lamp):
print(" [{}] PermaProj '{}'".format(lamp.type, obj.name)) self._report.msg("[{}] PermaProj '{}'", lamp.type, obj.name, indent=2)
permaProjs.append(pl_light) permaProjs.append(pl_light)
else: else:
print(" [{}] PermaLight '{}'".format(lamp.type, obj.name)) self._report.msg("[{}] PermaLight '{}'", lamp.type, obj.name, indent=2)
permaLights.append(pl_light) permaLights.append(pl_light)
return (permaLights, permaProjs) return (permaLights, permaProjs)
@ -308,3 +309,7 @@ class LightConverter:
@property @property
def mgr(self): def mgr(self):
return self._exporter().mgr return self._exporter().mgr
@property
def _report(self):
return self._exporter().report

1
korman/korlib/__init__.py

@ -71,4 +71,5 @@ except ImportError:
size = stream.readInt() size = stream.readInt()
return (header, size) return (header, size)
else: else:
from .console import ConsoleToggler
from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY

83
korman/korlib/console.py

@ -0,0 +1,83 @@
# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ctypes
import math
import sys
class ConsoleToggler:
_instance = None
def __init__(self, want_console=None):
if want_console is not None:
self._console_wanted = want_console
def __new__(cls, want_console=None):
if cls._instance is None:
assert want_console is not None
cls._instance = object.__new__(cls)
cls._instance._console_was_visible = cls.is_console_visible()
cls._instance._console_wanted = want_console
cls._instance._context_active = False
cls._instance.keep_console = False
return cls._instance
def __enter__(self):
if self._context_active:
raise RuntimeError("ConsoleToggler context manager is not reentrant")
self._console_visible = self.is_console_visible()
self._context_active = True
self.activate_console()
return self
def __exit__(self, type, value, traceback):
if not self._console_was_visible and self._console_wanted:
if self.keep_console:
# Blender thinks the console is currently not visible. However, it actually is.
# So, we will fire off the toggle operator to keep Blender's internal state valid
bpy.ops.wm.console_toggle()
else:
self.hide_console()
self._context_active = False
self.keep_console = False
return False
def activate_console(self):
if sys.platform == "win32":
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
if self._console_wanted:
ctypes.windll.user32.ShowWindow(hwnd, 1)
if self._console_was_visible or self._console_wanted:
ctypes.windll.user32.BringWindowToTop(hwnd)
@staticmethod
def hide_console():
if sys.platform == "win32":
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
ctypes.windll.user32.ShowWindow(hwnd, 0)
@staticmethod
def is_console_visible():
if sys.platform == "win32":
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
return bool(ctypes.windll.user32.IsWindowVisible(hwnd))
@staticmethod
def is_platform_supported():
# If you read Blender's source code, GHOST_toggleConsole (the "Toggle System Console" menu
# item) is only implemented on Windows. The majority of our audience is on Windows as well,
# so I honestly don't see this as an issue...
return sys.platform == "win32"

8
korman/korlib/texture.py

@ -76,14 +76,14 @@ class GLTexture:
# It will simplify our state tracking a bit. # It will simplify our state tracking a bit.
bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_GENERATE_MIPMAP, 1) bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_GENERATE_MIPMAP, 1)
def get_level_data(self, level=0, calc_alpha=False, bgra=False, quiet=False, fast=False): def get_level_data(self, level=0, calc_alpha=False, bgra=False, report=None, fast=False):
"""Gets the uncompressed pixel data for a requested mip level, optionally calculating the alpha """Gets the uncompressed pixel data for a requested mip level, optionally calculating the alpha
channel from the image color data channel from the image color data
""" """
width = self._get_tex_param(bgl.GL_TEXTURE_WIDTH, level) width = self._get_tex_param(bgl.GL_TEXTURE_WIDTH, level)
height = self._get_tex_param(bgl.GL_TEXTURE_HEIGHT, level) height = self._get_tex_param(bgl.GL_TEXTURE_HEIGHT, level)
if not quiet: if report is not None:
print(" Level #{}: {}x{}".format(level, width, height)) report.msg("Level #{}: {}x{}", level, width, height, indent=2)
# Grab the image data # Grab the image data
size = width * height * 4 size = width * height * 4
@ -138,7 +138,7 @@ class GLTexture:
@property @property
def has_alpha(self): def has_alpha(self):
data = self.get_level_data(quiet=True, fast=True) data = self.get_level_data(report=None, fast=True)
for i in range(3, len(data), 4): for i in range(3, len(data), 4):
if data[i] != 255: if data[i] != 255:
return True return True

6
korman/nodes/node_conditions.py

@ -427,18 +427,18 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
suffix = "Exit" suffix = "Exit"
theName = "{}_{}_{}".format(self.id_data.name, self.name, suffix) theName = "{}_{}_{}".format(self.id_data.name, self.name, suffix)
print(" [LogicModifier '{}']".format(theName)) exporter.report.msg("[LogicModifier '{}']", theName, indent=2)
logicKey = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so) logicKey = exporter.mgr.find_create_key(plLogicModifier, name=theName, so=so)
logicmod = logicKey.object logicmod = logicKey.object
logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True) logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True)
logicmod.notify = self.generate_notify_msg(exporter, so, "satisfies") logicmod.notify = self.generate_notify_msg(exporter, so, "satisfies")
# Now, the detector objects # Now, the detector objects
print(" [ObjectInVolumeDetector '{}']".format(theName)) exporter.report.msg("[ObjectInVolumeDetector '{}']", theName, indent=2)
detKey = exporter.mgr.find_create_key(plObjectInVolumeDetector, name=theName, so=so) detKey = exporter.mgr.find_create_key(plObjectInVolumeDetector, name=theName, so=so)
det = detKey.object det = detKey.object
print(" [VolumeSensorConditionalObject '{}']".format(theName)) exporter.report.msg("[VolumeSensorConditionalObject '{}']", theName, indent=2)
volKey = exporter.mgr.find_create_key(plVolumeSensorConditionalObject, name=theName, so=so) volKey = exporter.mgr.find_create_key(plVolumeSensorConditionalObject, name=theName, so=so)
volsens = volKey.object volsens = volKey.object

13
korman/operators/op_export.py

@ -22,6 +22,7 @@ import pstats
from .. import exporter from .. import exporter
from ..properties.prop_world import PlasmaAge from ..properties.prop_world import PlasmaAge
from ..properties.modifiers.logic import game_versions from ..properties.modifiers.logic import game_versions
from ..korlib import ConsoleToggler
class ExportOperator(bpy.types.Operator): class ExportOperator(bpy.types.Operator):
"""Exports ages for Cyan Worlds' Plasma Engine""" """Exports ages for Cyan Worlds' Plasma Engine"""
@ -45,6 +46,14 @@ class ExportOperator(bpy.types.Operator):
"description": "Version of the Plasma Engine to target", "description": "Version of the Plasma Engine to target",
"default": "pvPots", # This should be changed when moul is easier to target! "default": "pvPots", # This should be changed when moul is easier to target!
"items": game_versions}), "items": game_versions}),
"verbose": (BoolProperty, {"name": "Display Verbose Log",
"description": "Shows the verbose export log in the console",
"default": False}),
"show_console": (BoolProperty, {"name": "Display Log Console",
"description": "Forces the Blender System Console open during the export",
"default": True}),
} }
# This wigs out and very bad things happen if it's not directly on the operator... # This wigs out and very bad things happen if it's not directly on the operator...
@ -58,6 +67,10 @@ class ExportOperator(bpy.types.Operator):
# The crazy mess we're doing with props on the fly means we have to explicitly draw them :( # The crazy mess we're doing with props on the fly means we have to explicitly draw them :(
layout.prop(age, "version") layout.prop(age, "version")
layout.prop(age, "bake_lighting") layout.prop(age, "bake_lighting")
row = layout.row()
row.enabled = ConsoleToggler.is_platform_supported()
row.prop(age, "show_console")
layout.prop(age, "verbose")
layout.prop(age, "profile_export") layout.prop(age, "profile_export")
def __getattr__(self, attr): def __getattr__(self, attr):

4
korman/properties/modifiers/render.py

@ -427,10 +427,10 @@ class PlasmaVisControl(PlasmaModifierProperties):
else: else:
this_sv = bo.plasma_modifiers.softvolume this_sv = bo.plasma_modifiers.softvolume
if this_sv.enabled: if this_sv.enabled:
print(" [VisRegion] I'm a SoftVolume myself :)") exporter.report.msg("[VisRegion] I'm a SoftVolume myself :)", indent=1)
rgn.region = this_sv.get_key(exporter, so) rgn.region = this_sv.get_key(exporter, so)
else: else:
print(" [VisRegion] SoftVolume '{}'".format(self.softvolume)) exporter.report.msg("[VisRegion] SoftVolume '{}'", self.softvolume, indent=1)
sv_bo = bpy.data.objects.get(self.softvolume, None) sv_bo = bpy.data.objects.get(self.softvolume, None)
if sv_bo is None: if sv_bo is None:
raise ExportError("'{}': Invalid object '{}' for VisControl soft volume".format(bo.name, self.softvolume)) raise ExportError("'{}': Invalid object '{}' for VisControl soft volume".format(bo.name, self.softvolume))

2
korman/properties/modifiers/sound.py

@ -173,7 +173,7 @@ class PlasmaSound(bpy.types.PropertyGroup):
name = "Sfx-{}_{}".format(so.key.name, self.sound_data) name = "Sfx-{}_{}".format(so.key.name, self.sound_data)
else: else:
name = "Sfx-{}_{}:{}".format(so.key.name, self.sound_data, channel) name = "Sfx-{}_{}:{}".format(so.key.name, self.sound_data, channel)
print(" [{}] {}".format(pClass.__name__[2:], name)) exporter.report.msg("[{}] {}", pClass.__name__[2:], name, indent=1)
sound = exporter.mgr.find_create_object(pClass, so=so, name=name) sound = exporter.mgr.find_create_object(pClass, so=so, name=name)
# If this object is a soft volume itself, we will use our own soft region. # If this object is a soft volume itself, we will use our own soft region.

Loading…
Cancel
Save