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;
}
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;
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height);
GLenum fmt = bgra ? GL_BGRA_EXT : GL_RGBA;
if (!quiet)
PySys_WriteStdout(" Level #%i: %ix%i\n", level, width, height);
// Print out the debug message
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;
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 char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("bgra"),
_pycs("quiet"), _pycs("fast"), NULL };
_pycs("report"), _pycs("fast"), NULL };
GLint level = 0;
bool calc_alpha = false;
bool bgra = false;
bool quiet = false;
PyObject* report = nullptr;
bool fast = false;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ibbbb", kwlist, &level, &calc_alpha, &bgra, &quiet, &fast)) {
PyErr_SetString(PyExc_TypeError, "get_level_data expects an optional int, bool, bool, bool, bool");
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, obejct, bool");
return NULL;
}
_LevelData data = _get_level_data(self, level, bgra, quiet);
_LevelData data = _get_level_data(self, level, bgra, report);
if (fast)
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*) {
_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) {
if (data.m_data[i] != 255) {
delete[] data.m_data;

2
korman/__init__.py

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

79
korman/exporter/convert.py

@ -14,6 +14,7 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
from ..korlib import ConsoleToggler
from pathlib import Path
from PyHSPlasma import *
import time
@ -42,19 +43,28 @@ class Exporter:
return Path(self._op.filepath).stem
def run(self):
with logger.ExportLogger(self._op.filepath) as _log:
print("Exporting '{}.age'".format(self.age_name))
start = time.perf_counter()
log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger
with ConsoleToggler(self._op.show_console), log(self._op.filepath) as self.report:
# Step 0: Init export resmgr and stuff
self.mgr = manager.ExportManager(self)
self.mesh = mesh.MeshConverter(self)
self.report = logger.ExportAnalysis()
self.physics = physics.PhysicsConverter(self)
self.light = rtlight.LightConverter(self)
self.animation = animation.AnimationConverter(self)
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
self._export_age_info()
@ -91,17 +101,18 @@ class Exporter:
# Step 5.1: Save out the export report.
# If the export fails and this doesn't save, we have bigger problems than
# these little warnings and notices.
self.report.progress_end()
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):
oven = etlight.LightBaker()
oven = etlight.LightBaker(self.report)
oven.bake_static_lighting(self._objects)
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
age = bpy.context.scene.world.plasma_age
pages_enabled = frozenset([page.name for page in age.pages if page.enabled])
@ -136,6 +147,7 @@ class Exporter:
self._objects.append(obj)
elif page not in all_pages:
error.add(page, obj.name)
inc_progress()
error.raise_if_error()
def _export_age_info(self):
@ -163,7 +175,7 @@ class Exporter:
parent = bo.parent
if parent is not None:
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.addChild(so.key)
else:
@ -187,8 +199,13 @@ class Exporter:
return so.coord.object
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:
print("\n[SceneObject '{}']".format(bl_obj.name))
log_msg("\n[SceneObject '{}']".format(bl_obj.name))
# First pass: do things specific to this object type.
# note the function calls: to export a MESH, it's _export_mesh_blobj
@ -196,10 +213,10 @@ class Exporter:
try:
export_fn = getattr(self, export_fn)
except AttributeError:
print("WARNING: '{}' is a Plasma Object of Blender type '{}'".format(bl_obj.name, bl_obj.type))
print("... And I have NO IDEA what to do with that! Tossing.")
self.report.warn("""'{}' is a Plasma Object of Blender type '{}'
... And I have NO IDEA what to do with that! Tossing.""".format(bl_obj.name, bl_obj.type))
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.
# 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...
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)
inc_progress()
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
@ -227,21 +245,33 @@ class Exporter:
if bo.data.materials:
self.mesh.export_object(bo)
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):
print("\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_advance()
self.report.progress_range = len(self.want_node_trees)
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:
print(" NodeTree '{}'".format(tree))
self.report.msg("NodeTree '{}'", tree, indent=1)
bpy.data.node_groups[tree].export(self, bo, so)
inc_progress()
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 mod in bl_obj.plasma_modifiers.modifiers:
if mod.enabled:
self.actors.update(mod.harvest_actors())
inc_progress()
# This is a little hacky, but it's an edge case... I guess?
# We MUST have CoordinateInterfaces for EnvironmentMaps (DCMs, bah)
@ -251,6 +281,7 @@ class Exporter:
viewpt = envmap.viewpoint_object
if viewpt is not None:
self.actors.add(viewpt.name)
inc_progress()
def has_coordiface(self, bo):
if bo.type in {"CAMERA", "EMPTY", "LAMP"}:
@ -269,7 +300,9 @@ class Exporter:
return False
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
for bl_obj in self._objects:
@ -292,5 +325,5 @@ class Exporter:
for mod in bl_obj.plasma_modifiers.modifiers:
proc = getattr(mod, "post_export", None)
if proc is not None:
print(" '{}' modifier '{}'".format(bl_obj.name, mod.key_name))
proc(self, bl_obj, sceneobject)
inc_progress()

60
korman/exporter/etlight.py

@ -16,6 +16,7 @@
import bpy
from bpy.app.handlers import persistent
from .logger import ExportProgressLogger
from .mesh import _VERTEX_COLOR_LAYERS
from ..helpers import *
@ -24,10 +25,27 @@ _NUM_RENDER_LAYERS = 20
class LightBaker:
"""ExportTime Lighting"""
def __init__(self):
def __init__(self, report=None):
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 = {}
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):
render = bpy.context.scene.render
toggle.track(render, "use_textures", False)
@ -63,7 +81,7 @@ class LightBaker:
def bake_static_lighting(self, objs):
"""Bakes all static lighting for Plasma geometry"""
print("\nBaking Static Lighting...")
self._report.msg("\nBaking Static Lighting...")
bake = self._harvest_bakable_objects(objs)
with GoodNeighbor() as toggle:
@ -77,43 +95,51 @@ class LightBaker:
return result
def _bake_static_lighting(self, bake, toggle):
inc_progress = self._report.progress_increment
# Step 0.9: Make all layers visible.
# This prevents context operators from phailing.
bpy.context.scene.layers = (True,) * _NUM_RENDER_LAYERS
# 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():
if key[0] == "lightmap":
for i in range(len(bake[key])-1, -1, -1):
obj = bake[key][i]
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)
elif key[0] == "vcol":
for i in range(len(bake[key])-1, -1, -1):
obj = bake[key][i]
if not self._prep_for_vcols(obj, toggle):
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)
else:
raise RuntimeError(key[0])
print(" ...")
inc_progress()
self._report.msg(" ...")
# Step 2: BAKE!
self._report.progress_advance()
self._report.progress_range = len(bake)
for key, value in bake.items():
if not value:
continue
if key[0] == "lightmap":
print(" {} Lightmap(s) [H:{:X}]".format(len(value), hash(key)))
self._bake_lightmaps(value, key[1:])
elif key[0] == "vcol":
print(" {} Crap Light(s)".format(len(value)))
self._bake_vcols(value)
else:
raise RuntimeError(key[0])
if value:
if key[0] == "lightmap":
self._report.msg("{} Lightmap(s) [H:{:X}]", len(value), hash(key), indent=1)
self._bake_lightmaps(value, key[1:])
elif key[0] == "vcol":
self._report.msg("{} Crap Light(s)", len(value), indent=1)
self._bake_vcols(value)
else:
raise RuntimeError(key[0])
inc_progress()
# Return how many thingos we baked
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
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
from ..korlib import ConsoleToggler
from pathlib import Path
import sys
import threading
import time
class ExportAnalysis:
"""This is used to collect artist action items from the export process. You can warn about
portability issues, possible oversights, etc. The benefit here is that the user doesn't have
to look through all of the gobbledygook in the export log.
"""
_HEADING_SIZE = 50
_MAX_ELIPSES = 3
_MAX_TIME_UNTIL_ELIPSES = 2.0
_porting = []
_warnings = []
class _ExportLogger:
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):
# TODO
pass
def port(self, message, indent=0):
self._porting.append(message)
print(" " * indent, end="")
print("PORTING: {}".format(message))
def warn(self, *args, **kwargs):
assert args
indent = kwargs.get("indent", 0)
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:
"""Yet Another Logger(TM)"""
# Long running operations like the Blender bake_image call make it seem like we've hung
# 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):
# 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 = Path(ageFile)
my_path = my_path.with_name("{}_export".format(my_path.stem)).with_suffix(".log")
self._file = open(str(my_path), "w")
# Progress manager
self._progress_alive = False
self._progress_steps = []
self._step_id = -1
self._step_max = 0
self._step_progress = 0
self._time_start_step = 0
for i in dir(self._file):
if not hasattr(self, i):
setattr(self, i, getattr(self._file, i))
def __exit__(self, type, value, traceback):
if value is not None:
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):
self._stdout, sys.stdout = sys.stdout, self._file
self._stderr, sys.stderr = sys.stderr, self._file
def progress_add_step(self, name):
assert self._step_id == -1
self._progress_steps.append(name)
def __exit__(self, type, value, traceback):
sys.stdout = self._stdout
sys.stderr = self._stderr
def progress_advance(self):
"""Advances the progress bar to the next step"""
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):
self._file.flush()
self._stdout.flush()
self._stderr.flush()
def _progress_print_heading(self, text=None):
if text:
num_chars = len(text)
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):
self._file.write(str)
self._stdout.write(str)
def _progress_print_step(self, done=False, error=False):
with self._print_condition:
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):
self._file.writelines(seq)
self._stdout.writelines(seq)
line = "{}\t(step {}/{}): {}".format(self._progress_steps[self._step_id], self._step_id+1,
len(self._progress_steps), stage)
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):
"""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)
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
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
unique_name = "{}_WaveSet7".format(bm.name)
@ -215,7 +215,7 @@ class MaterialConverter:
def export_bumpmap_slot(self, bo, bm, hsgmat, slot, idx):
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
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):
if name is None:
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)
if bm is not None and not slot.use_map_normal:
self._propagate_material_settings(bm, layer)
@ -274,10 +274,10 @@ class MaterialConverter:
for i, uvchan in enumerate(bo.data.uv_layers):
if uvchan.name == slot.uv_layer:
layer.UVWSrc = i
print(" Using UV Map #{} '{}'".format(i, name))
self._report.msg("Using UV Map #{} '{}'", i, name, indent=3)
break
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
xform = hsMatrix44()
@ -447,7 +447,8 @@ class MaterialConverter:
name = "{}_DynEnvMap".format(viewpt.name)
pl_env = self._mgr.find_object(pl_class, bl=bo, name=name)
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):
pl_env.addTargetNode(self._mgr.find_key(plSceneObject, bl=bo))
pl_env.addMatLayer(layer.key)
@ -457,7 +458,7 @@ class MaterialConverter:
oRes = bl_env.resolution
eRes = helpers.ensure_power_of_two(oRes)
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
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_opacity_start=layer_props.detail_opacity_start, detail_opacity_stop=layer_props.detail_opacity_stop)
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,]
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)
def _export_texture_type_none(self, bo, layer, texture):
@ -609,16 +611,20 @@ class MaterialConverter:
"""This exports an externally prepared layer and image"""
key = _Texture(image=image)
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,]
else:
print(" Found another user of '{}'".format(key))
self._report.msg("Found another user of '{}'", key, indent=2)
self._pending[key].append(layer.key)
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():
name = str(key)
print("\n[Mipmap '{}']".format(name))
self._report.msg("\n[Mipmap '{}']", name)
image = key.image
oWidth, oHeight = image.size
@ -628,7 +634,8 @@ class MaterialConverter:
eWidth = helpers.ensure_power_of_two(oWidth)
eHeight = helpers.ensure_power_of_two(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)
# Some basic mipmap settings.
@ -640,11 +647,11 @@ class MaterialConverter:
with helper as glimage:
if key.mipmap:
numLevels = glimage.num_levels
print(" Generating mip levels")
self._report.msg("Generating mip levels", indent=1)
glimage.generate_mipmap()
else:
numLevels = 1
print(" Stuffing image data")
self._report.msg("Stuffing image data", indent=1)
# Uncompressed bitmaps are BGRA
fmt = compression == plBitmap.kUncompressed
@ -653,7 +660,7 @@ class MaterialConverter:
# this mipmap for per-page textures :(
data = []
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
image.reload()
@ -663,9 +670,9 @@ class MaterialConverter:
mgr = self._mgr
pages = {}
print(" Adding to Layer(s)")
self._report.msg("Adding to Layer(s)", indent=1)
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
# If we haven't created this plMipmap in the page (either layer's page or Textures.prp),
@ -680,6 +687,7 @@ class MaterialConverter:
else:
mipmap = pages[page]
layer.object.texture = mipmap.key
inc_progress()
def get_materials(self, bo):
return self._obj2mat.get(bo, [])
@ -722,6 +730,10 @@ class MaterialConverter:
layer.runtime = utils.color(bm.diffuse_color)
layer.specular = utils.color(bm.specular_color)
@property
def _report(self):
return self._exporter().report
def _resize_image(self, image, width, height):
image.scale(width, height)
image.update()

20
korman/exporter/mesh.py

@ -174,11 +174,15 @@ class MeshConverter:
def finalize(self):
"""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 dspan in loc.values():
print("\n[DrawableSpans '{}']".format(dspan.key.name))
print(" Composing geometry data")
log_msg("[DrawableSpans '{}']", dspan.key.name, indent=1)
# This mega-function does a lot:
# 1. Converts SourceSpans (geospans) to Icicles and bakes geometry into plGBuffers
@ -186,10 +190,7 @@ class MeshConverter:
# 3. Builds the plSpaceTree
# 4. Clears the SourceSpans
dspan.composeGeometry(True, True)
# 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")
inc_progress()
def _export_geometry(self, bo, mesh, materials, geospans):
geodata = [_GeoData(len(mesh.vertices)) for i in materials]
@ -422,7 +423,8 @@ class MeshConverter:
_diindices = {}
for geospan, pass_index in geospans:
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)
if dspan not in _diindices:
_diindices[dspan] = [idx,]
@ -497,3 +499,7 @@ class MeshConverter:
@property
def _mgr(self):
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!
intens, attenEnd = self.convert_attenuation(bl)
if bl.falloff_type == "CONSTANT":
print(" Attenuation: No Falloff")
self._report.msg("Attenuation: No Falloff", indent=2)
pl.attenConst = intens
pl.attenLinear = 0.0
pl.attenQuadratic = 0.0
pl.attenCutoff = attenEnd
elif bl.falloff_type == "INVERSE_LINEAR":
print(" Attenuation: Inverse Linear")
self._report.msg("Attenuation: Inverse Linear", indent=2)
pl.attenConst = 1.0
pl.attenLinear = self.convert_attenuation_linear(intens, attenEnd)
pl.attenQuadratic = 0.0
pl.attenCutoff = attenEnd
elif bl.falloff_type == "INVERSE_SQUARE":
print(" Attenuation: Inverse Square")
self._report.msg("Attenuation: Inverse Square", indent=2)
pl.attenConst = 1.0
pl.attenLinear = 0.0
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))
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.depth = bl.size if bl.shape == "SQUARE" else bl.size_y
pl.height = bl.plasma_lamp.size_height
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)
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)
# Spot lights have a few more things...
@ -102,7 +102,7 @@ class LightConverter:
pl.falloff = 1.0
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):
bl_light = bo.data
@ -132,18 +132,18 @@ class LightConverter:
# Apply the colors
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)
else:
print(" Diffuse: OFF")
self._report.msg("Diffuse: OFF", indent=2)
pl_light.diffuse = hsColorRGBA(0.0, 0.0, 0.0, energy)
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.specular = hsColorRGBA(*spec_color)
else:
print(" Specular: OFF")
self._report.msg("Specular: OFF", indent=2)
pl_light.specular = hsColorRGBA(0.0, 0.0, 0.0, energy)
rtlamp = bl_light.plasma_lamp
@ -202,7 +202,7 @@ class LightConverter:
# 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.
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)
state = layer.state
@ -243,7 +243,7 @@ class LightConverter:
def find_material_light_keys(self, bo, bm):
"""Given a blender material, we find the keys of all matching Plasma RT Lights.
NOTE: We return a tuple of lists: ([permaLights], [permaProjs])"""
print(" Searching for runtime lights...")
self._report.msg("Searching for runtime lights...", indent=1)
permaLights = []
permaProjs = []
@ -272,16 +272,17 @@ class LightConverter:
break
else:
# 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
# This is probably where PermaLight vs PermaProj should be sorted out...
pl_light = self.get_light_key(obj, lamp, None)
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)
else:
print(" [{}] PermaLight '{}'".format(lamp.type, obj.name))
self._report.msg("[{}] PermaLight '{}'", lamp.type, obj.name, indent=2)
permaLights.append(pl_light)
return (permaLights, permaProjs)
@ -308,3 +309,7 @@ class LightConverter:
@property
def mgr(self):
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()
return (header, size)
else:
from .console import ConsoleToggler
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.
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
channel from the image color data
"""
width = self._get_tex_param(bgl.GL_TEXTURE_WIDTH, level)
height = self._get_tex_param(bgl.GL_TEXTURE_HEIGHT, level)
if not quiet:
print(" Level #{}: {}x{}".format(level, width, height))
if report is not None:
report.msg("Level #{}: {}x{}", level, width, height, indent=2)
# Grab the image data
size = width * height * 4
@ -138,7 +138,7 @@ class GLTexture:
@property
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):
if data[i] != 255:
return True

6
korman/nodes/node_conditions.py

@ -427,18 +427,18 @@ class PlasmaVolumeSensorNode(PlasmaNodeBase, bpy.types.Node):
suffix = "Exit"
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)
logicmod = logicKey.object
logicmod.setLogicFlag(plLogicModifier.kMultiTrigger, True)
logicmod.notify = self.generate_notify_msg(exporter, so, "satisfies")
# 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)
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)
volsens = volKey.object

13
korman/operators/op_export.py

@ -22,6 +22,7 @@ import pstats
from .. import exporter
from ..properties.prop_world import PlasmaAge
from ..properties.modifiers.logic import game_versions
from ..korlib import ConsoleToggler
class ExportOperator(bpy.types.Operator):
"""Exports ages for Cyan Worlds' Plasma Engine"""
@ -45,6 +46,14 @@ class ExportOperator(bpy.types.Operator):
"description": "Version of the Plasma Engine to target",
"default": "pvPots", # This should be changed when moul is easier to target!
"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...
@ -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 :(
layout.prop(age, "version")
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")
def __getattr__(self, attr):

4
korman/properties/modifiers/render.py

@ -427,10 +427,10 @@ class PlasmaVisControl(PlasmaModifierProperties):
else:
this_sv = bo.plasma_modifiers.softvolume
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)
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)
if sv_bo is None:
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)
else:
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)
# If this object is a soft volume itself, we will use our own soft region.

Loading…
Cancel
Save