diff --git a/.gitignore b/.gitignore
index 064fad8..4086529 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,6 @@ pip-log.txt
*.komodoproject
*.project
*.pydevproject
+
+# Korlib build
+korlib/build
diff --git a/korlib/CMakeLists.txt b/korlib/CMakeLists.txt
new file mode 100644
index 0000000..78eac51
--- /dev/null
+++ b/korlib/CMakeLists.txt
@@ -0,0 +1,30 @@
+project(korman)
+cmake_minimum_required(VERSION 2.8.9)
+
+find_package(HSPlasma REQUIRED)
+find_package(OpenGL REQUIRED)
+find_package(PythonLibs REQUIRED)
+
+include_directories(${HSPlasma_INCLUDE_DIRS})
+include_directories(${OPENGL_INCLUDE_DIR})
+include_directories(${PYTHON_INCLUDE_DIR})
+
+set(korlib_HEADERS
+ pyMipmap.h
+ utils.hpp
+)
+
+set(korlib_SOURCES
+ generate_mipmap.cpp
+ module.cpp
+)
+
+add_library(korlib SHARED ${korlib_HEADERS} ${korlib_SOURCES})
+target_link_libraries(korlib HSPlasma ${OPENGL_LIBRARIES} ${PYTHON_LIBRARIES})
+
+if(WIN32)
+ set_target_properties(korlib PROPERTIES SUFFIX ".pyd")
+endif(WIN32)
+
+source_group("Header Files" FILES ${korlib_HEADERS})
+source_group("Source Files" FILES ${korlib_SOURCES})
diff --git a/korlib/generate_mipmap.cpp b/korlib/generate_mipmap.cpp
new file mode 100644
index 0000000..1f1b536
--- /dev/null
+++ b/korlib/generate_mipmap.cpp
@@ -0,0 +1,213 @@
+/* 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 .
+ */
+
+#include
+#include
+#include
+#include
+
+#ifdef _WINDOWS
+# define NOMINMAX
+# define WIN32_LEAN_AND_MEAN
+# include
+
+# define GL_GENERATE_MIPMAP 0x8191
+#endif // _WINDOWS
+
+#include
+
+#include
+#include
+#include
+
+#include "pyMipmap.h"
+#include "utils.hpp"
+
+// ========================================================================
+
+class gl_loadimage
+{
+ bool m_weLoadedIt;
+ bool m_success;
+ GLint m_genMipMapState;
+ korlib::pyref m_image;
+
+public:
+ gl_loadimage(const korlib::pyref& image) : m_success(true), m_image(image)
+ {
+ size_t bindcode = korlib::getattr(image, "bindcode");
+ m_weLoadedIt = (bindcode == 0);
+ if (m_weLoadedIt) {
+ m_success = (korlib::call_method(image, "gl_load") == 0);
+ bindcode = korlib::getattr(image, "bindcode");
+ }
+ if (m_success) {
+ glBindTexture(GL_TEXTURE_2D, bindcode);
+ }
+
+ // We want to gen mipmaps
+ // GIANTLY GNARLY DISCLAIMER:
+ // This requires OpenGL 1.4, which is above Windows' "built-in" headers (1.1)
+ // It was also deprecated in 3.0, and removed in 3.1.
+ // In other words, we should probably use glGenerateMipmap (3.0) or Blender's scale function
+ glGetTexParameteriv(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, &m_genMipMapState);
+ glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
+ }
+
+ ~gl_loadimage()
+ {
+ if (m_success && m_weLoadedIt)
+ korlib::call_method(m_image, "gl_free");
+ glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, m_genMipMapState);
+ }
+
+ bool success() const { return m_success; }
+};
+
+// ========================================================================
+
+typedef std::tuple imagesize_t;
+
+/** Gets the dimensions of a Blender Image in pixels (WxH) */
+static imagesize_t get_image_size(PyObject* image)
+{
+ korlib::pyref size = PyObject_GetAttrString(image, "size");
+ size_t width = PyLong_AsSize_t(PySequence_GetItem(size, 0));
+ size_t height = PyLong_AsSize_t(PySequence_GetItem(size, 1));
+
+ return std::make_tuple(width, height);
+}
+
+static void resize_image(PyObject* image, size_t width, size_t height)
+{
+ korlib::pyref _w = PyLong_FromSize_t(width);
+ korlib::pyref _h = PyLong_FromSize_t(height);
+ korlib::pyref callable = korlib::getattr(image, "scale");
+ korlib::pyref result = PyObject_CallFunctionObjArgs(callable, _w, _h);
+}
+
+// ========================================================================
+
+static void stuff_mip_level(plMipmap* mipmap, size_t level, PyObject* image, bool alphaChannel, bool calcAlpha)
+{
+ GLint format = alphaChannel ? GL_RGBA : GL_RGB;
+ uint8_t bytesPerPixel = alphaChannel ? 4 : 3;
+
+ // How big is this doggone level?
+ GLint width, height;
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height);
+ print(" Level %d: %dx%d...", level, width, height);
+
+ // Grab the stuff from the place and the things
+ size_t dataSize = width * height * bytesPerPixel;
+ uint8_t* data = new uint8_t[dataSize]; // optimization: use stack for small images...
+ glGetTexImage(GL_TEXTURE_2D, level, format, GL_UNSIGNED_BYTE, data);
+
+ // Need to calculate alpha?
+ if (alphaChannel && calcAlpha) {
+ uint8_t* ptr = data;
+ uint8_t* end = data + dataSize;
+ while (ptr < end) {
+ uint8_t r = *ptr++;
+ uint8_t g = *ptr++;
+ uint8_t b = *ptr++;
+ *ptr++ = (r + g + b) / 255;
+ }
+ }
+
+ // Stuff into plMipmap. Unfortunately, it's not smart enough to just work, so we have to do
+ // a little bit of TESTing here.
+ try {
+ mipmap->CompressImage(level, data, dataSize);
+ } catch (hsNotImplementedException&) {
+ mipmap->setLevelData(level, data, dataSize);
+ }
+ delete[] data;
+}
+
+// ========================================================================
+
+extern "C" PyObject* generate_mipmap(PyObject*, PyObject* args)
+{
+ // Convert some of this Python nonsense to good old C
+ PyObject* blTexImage = nullptr; // unchecked... better be right
+ PyObject* pymm = nullptr;
+ if (PyArg_ParseTuple(args, "OO", &blTexImage, &pymm) && blTexImage && pymm) {
+ // Since we can't link with PyHSPlasma easily, let's do some roundabout type-checking
+ korlib::pyref classindex = PyObject_CallMethod(pymm, "ClassIndex", "");
+ static short mipmap_classindex = plFactory::ClassIndex("plMipmap");
+
+ if (PyLong_AsLong(classindex) != mipmap_classindex) {
+ PyErr_SetString(PyExc_TypeError, "generate_mipmap expects a Blender ImageTexture and a plMipmap");
+ return nullptr;
+ }
+ } else {
+ PyErr_SetString(PyExc_TypeError, "generate_mipmap expects a Blender ImageTexture and a plMipmap");
+ return nullptr;
+ }
+
+ // Grab the important stuff
+ plMipmap* mipmap = ((pyMipmap*)pymm)->fThis;
+ korlib::pyref blImage = korlib::getattr(blTexImage, "image");
+ bool makeMipMap = korlib::getattr(blTexImage, "use_mipmap");
+ bool useAlpha = korlib::getattr(blTexImage, "use_alpha");
+ bool calcAlpha = korlib::getattr(blTexImage, "use_calculate_alpha");
+
+ // Okay, so, here are the assumptions.
+ // We assume that the Korman Python code as already created the mipmap's key and named it appropriately
+ // So, if we're mipmapping nb01StoneSquareCobble.tga -> nb01StoneSquareCobble.dds as the key name
+ // What we now need to do:
+ // 1) Make sure this is a POT texture (if not, call scale on the Blender Image)
+ // 2) Check calcAlpha and all that rubbish--det DXT1/DXT5/uncompressed
+ // 3) "Create" the plMipmap--this allocates internal buffers and such
+ // 4) Loop through the levels, going down through the POTs and fill in the pixel data
+ // The reason we do this in C instead of python is because it's a lot of iterating over a lot of
+ // floating point data (we have to convert to RGB8888, joy). Should be faster here!
+ print("Exporting '%s'...", mipmap->getKey()->getName().cstr());
+
+ // Step 1: Resize to POT (if needed) -- don't rely on GLU for this because it may not suppport
+ // NPOT if we're being run on some kind of dinosaur...
+ imagesize_t dimensions = get_image_size(blImage);
+ size_t width = pow(2., korlib::log2(static_cast(std::get<0>(dimensions))));
+ size_t height = pow(2., korlib::log2(static_cast(std::get<1>(dimensions))));
+ if (std::get<0>(dimensions) != width || std::get<1>(dimensions) != height) {
+ print("\tImage is not a POT (%dx%d)... resizing to %dx%d", std::get<0>(dimensions),
+ std::get<1>(dimensions), width, height);
+ resize_image(blImage, width, height);
+ }
+
+ // Steps 2+3: Translate flags and pass to plMipmap::Create
+ // TODO: PNG compression for lossless images
+ uint8_t numLevels = (makeMipMap) ? 0 : 1; // 0 means "you figure it out"
+ uint8_t compType = (makeMipMap) ? plBitmap::kDirectXCompression : plBitmap::kUncompressed;
+ bool alphaChannel = useAlpha || calcAlpha;
+ mipmap->Create(width, height, numLevels, compType, plBitmap::kRGB8888, alphaChannel ? plBitmap::kDXT5 : plBitmap::kDXT1);
+
+ // Step 3.9: Load the image into OpenGL
+ gl_loadimage guard(blImage);
+ if (!guard.success()) {
+ PyErr_SetString(PyExc_RuntimeError, "failed to load image into OpenGL");
+ return nullptr;
+ }
+
+ // Step 4: Now it's a matter of looping through all the levels and exporting the image
+ for (size_t i = 0; i < mipmap->getNumLevels(); ++i) {
+ stuff_mip_level(mipmap, i, blImage, alphaChannel, calcAlpha);
+ }
+
+ Py_RETURN_NONE;
+}
diff --git a/korlib/module.cpp b/korlib/module.cpp
new file mode 100644
index 0000000..a7579f5
--- /dev/null
+++ b/korlib/module.cpp
@@ -0,0 +1,48 @@
+/* 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 .
+ */
+
+#include
+
+// ========================================================================
+
+extern "C" PyObject* generate_mipmap(PyObject*, PyObject*);
+
+// ========================================================================
+
+static struct PyMethodDef s_korlibMethods[] =
+{
+ { "generate_mipmap", generate_mipmap, METH_VARARGS, "Generates a new plMipmap from a Blender ImageTexture" },
+ { nullptr, nullptr, 0, nullptr },
+};
+
+static struct PyModuleDef s_korlibModule = {
+ PyModuleDef_HEAD_INIT,
+ "korlib",
+ NULL,
+ -1,
+ s_korlibMethods
+};
+
+#define ADD_CONSTANT(module, name) \
+ PyModule_AddIntConstant(module, #name, korlib::name)
+
+PyMODINIT_FUNC PyInit_korlib()
+{
+ PyObject* module = PyModule_Create(&s_korlibModule);
+
+ // Done!
+ return module;
+}
diff --git a/korlib/pyMipmap.h b/korlib/pyMipmap.h
new file mode 100644
index 0000000..83b671c
--- /dev/null
+++ b/korlib/pyMipmap.h
@@ -0,0 +1,27 @@
+/* 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 .
+ */
+
+/**
+ * \file Declarations required to interop with PyHSPlasma's pyMipmap
+ */
+
+#include
+
+typedef struct {
+ PyObject_HEAD
+ class plMipmap* fThis;
+ bool fPyOwned;
+} pyMipmap;
diff --git a/korlib/utils.hpp b/korlib/utils.hpp
new file mode 100644
index 0000000..5a568f5
--- /dev/null
+++ b/korlib/utils.hpp
@@ -0,0 +1,89 @@
+/* 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 .
+ */
+
+#ifndef __KORLIB_UTILS_HPP
+#define __KORLIB_UTILS_HPP
+
+#include
+
+#define print(fmt, ...) PySys_WriteStdout(" " fmt "\n", __VA_ARGS__)
+
+namespace korlib
+{
+ /** RAII for PyObject pointers */
+ class pyref
+ {
+ PyObject* _ref;
+ public:
+ pyref(PyObject* o) : _ref(o) { }
+ pyref(const pyref& copy) : _ref((PyObject*)copy)
+ {
+ Py_INCREF(_ref);
+ }
+
+ ~pyref()
+ {
+ Py_XDECREF(_ref);
+ }
+
+ operator PyObject*() const { return _ref; }
+ };
+
+ template
+ T call_method(PyObject* o, const char* method);
+
+ template<>
+ size_t call_method(PyObject* o, const char* method)
+ {
+ pyref retval = PyObject_CallMethod(o, const_cast(method), "");
+ if ((PyObject*)retval)
+ return PyLong_AsSize_t(retval);
+ else
+ return static_cast(-1);
+ }
+
+ template
+ T getattr(PyObject* o, const char* name);
+
+ template<>
+ bool getattr(PyObject* o, const char* name)
+ {
+ pyref attr = PyObject_GetAttrString(o, name);
+ return PyLong_AsLong(attr) != 0;
+ }
+
+ template<>
+ PyObject* getattr(PyObject* o, const char* name)
+ {
+ return PyObject_GetAttrString(o, name);
+ }
+
+ template<>
+ size_t getattr(PyObject* o, const char* name)
+ {
+ pyref attr = PyObject_GetAttrString(o, name);
+ return PyLong_AsSize_t(attr);
+ }
+
+ /** MSVC++ is not C99 compliant :( */
+ double log2(double v)
+ {
+ static double hack = log(2.);
+ return log(v) / hack;
+ }
+};
+
+#endif // __KORLIB_UTILS_HPP
diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py
index b6d0dcd..95ecba6 100644
--- a/korman/exporter/convert.py
+++ b/korman/exporter/convert.py
@@ -40,7 +40,7 @@ class Exporter:
# Step 0: Init export resmgr and stuff
self.mgr = manager.ExportManager(globals()[self._op.version])
- self.mesh = mesh.MeshConverter(self.mgr)
+ self.mesh = mesh.MeshConverter(self)
self.report = logger.ExportAnalysis()
# Step 1: Gather a list of objects that we need to export
@@ -101,7 +101,7 @@ class Exporter:
if childobj:
parent = bo.parent
if parent.plasma_object.enabled:
- print("\tAttaching to parent SceneObject '{}'".format(parent.name))
+ print(" Attaching to parent SceneObject '{}'".format(parent.name))
# Instead of exporting a skeleton now, we'll just make an orphaned CI.
# The bl_obj export will make this work.
@@ -139,7 +139,7 @@ class Exporter:
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.")
continue
- print("\tBlender Object '{}' of type '{}'".format(bl_obj.name, bl_obj.type))
+ print(" Blender Object '{}' of type '{}'".format(bl_obj.name, bl_obj.type))
# 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
@@ -154,4 +154,7 @@ class Exporter:
pass
def _export_mesh_blobj(self, so, bo):
- so.draw = self.mesh.export_object(bo)
+ if bo.data.materials:
+ so.draw = self.mesh.export_object(bo)
+ else:
+ print(" No material(s) on the ObData, so no drawables")
diff --git a/korman/exporter/explosions.py b/korman/exporter/explosions.py
index 5e08df5..6c3bdf0 100644
--- a/korman/exporter/explosions.py
+++ b/korman/exporter/explosions.py
@@ -18,6 +18,13 @@ class ExportError(Exception):
super(Exception, self).__init__(value)
+class TooManyUVChannelsError(ExportError):
+ def __init__(self, obj, mat):
+ msg = "There are too many UV Textures on the material '{}' associated with object '{}'.".format(
+ mat.name, obj.name)
+ super(ExportError, self).__init__(msg)
+
+
class TooManyVerticesError(ExportError):
def __init__(self, mesh, matname, vertcount):
msg = "There are too many vertices ({}) on the mesh data '{}' associated with material '{}'".format(
@@ -41,3 +48,8 @@ class UndefinedPageError(ExportError):
def raise_if_error(self):
if self.mistakes:
raise self
+
+
+class UnsupportedTextureError(ExportError):
+ def __init__(self, texture, material):
+ super(ExportError, self).__init__("Cannot export texture '{}' on material '{}' -- unsupported type '{}'".format(texture.name, texture.type, material.name))
diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py
index a8633a3..0f9dec5 100644
--- a/korman/exporter/manager.py
+++ b/korman/exporter/manager.py
@@ -90,7 +90,7 @@ class ExportManager:
def create_builtins(self, age, textures):
# BuiltIn.prp
if bpy.context.scene.world.plasma_age.age_sdl:
- builtin = self.create_page(age, "BuiltIn", -1, True)
+ builtin = self.create_page(age, "BuiltIn", -2, True)
pfm = self.add_object(plPythonFileMod, name="VeryVerySpecialPythonFileMod", loc=builtin)
pfm.filename = age
sdl = self.add_object(plSceneObject, name="AgeSDLHook", loc=builtin)
@@ -98,7 +98,7 @@ class ExportManager:
# Textures.prp
if textures:
- self.create_page(age, "Textures", -2, True)
+ self.create_page(age, "Textures", -1, True)
def create_page(self, age, name, id, builtin=False):
location = plLocation(self.mgr.getVer())
diff --git a/korman/exporter/material.py b/korman/exporter/material.py
new file mode 100644
index 0000000..0727cb0
--- /dev/null
+++ b/korman/exporter/material.py
@@ -0,0 +1,130 @@
+# 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 .
+
+import bpy
+import korlib
+from PyHSPlasma import *
+import weakref
+
+from . import explosions
+from . import utils
+
+class MaterialConverter:
+ _hsbitmaps = {}
+
+ def __init__(self, exporter):
+ self._exporter = weakref.ref(exporter)
+
+ def export_material(self, bo, bm):
+ """Exports a Blender Material as an hsGMaterial"""
+ print(" Exporting Material '{}'".format(bm.name))
+
+ hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo)
+ self._export_texture_slots(bo, bm, hsgmat)
+
+ # Plasma makes several assumptions that every hsGMaterial has at least one layer. If this
+ # material had no Textures, we will need to initialize a default layer
+ if not hsgmat.layers:
+ layer = self._mgr.add_object(plLayer, name="{}_AutoLayer".format(bm.name), bl=bo)
+ self._propagate_material_settings(bm, layer)
+ hsgmat.addLayer(layer.key)
+
+ # Looks like we're done...
+ return hsgmat.key
+
+ def _export_texture_slots(self, bo, bm, hsgmat):
+ for slot in bm.texture_slots:
+ if slot is None or not slot.use:
+ continue
+
+ name = "{}_{}".format(bm.name, slot.name)
+ print(" Exporting Plasma Layer '{}'".format(name))
+ layer = self._mgr.add_object(plLayer, name=name, bl=bo)
+ self._propagate_material_settings(bm, layer)
+
+ # UVW Channel
+ for i, uvchan in enumerate(bo.data.tessface_uv_textures):
+ if uvchan.name == slot.uv_layer:
+ layer.UVWSrc = i
+ print(" Using UV Map #{} '{}'".format(i, name))
+ break
+ else:
+ print(" No UVMap specified... Blindly using the first one, maybe it exists :|")
+
+ # General texture flags and such
+ texture = slot.texture
+ # ...
+
+ # Export the specific texture type
+ export_fn = "_export_texture_type_{}".format(texture.type.lower())
+ if not hasattr(self, export_fn):
+ raise explosions.UnsupportedTextureError(texture, bm)
+ getattr(self, export_fn)(bo, hsgmat, layer, texture)
+ hsgmat.addLayer(layer.key)
+
+ def _export_texture_type_image(self, bo, hsgmat, layer, texture):
+ """Exports a Blender ImageTexture to a plLayer"""
+
+ # First, let's apply any relevant flags
+ state = layer.state
+ if texture.invert_alpha:
+ state.blendFlags |= hsGMatState.kBlendInvertAlpha
+
+ # Now, let's export the plBitmap
+ # If the image is None (no image applied in Blender), we assume this is a plDynamicTextMap
+ # Otherwise, we create a plMipmap and call into korlib to export the pixel data
+ if texture.image is None:
+ bitmap = self.add_object(plDynamicTextMap, name="{}_DynText".format(layer.key.name), bl=bo)
+ else:
+ # blender likes to create lots of spurious .0000001 objects :/
+ name = texture.image.name
+ name = name[:name.find('.')]
+ if texture.use_mipmap:
+ name = "{}.dds".format(name)
+ else:
+ name = "{}.bmp".format(name)
+
+ if name in self._hsbitmaps:
+ # well, that was easy...
+ print(" Using '{}'".format(name))
+ layer.texture = self._hsbitmaps[name].key
+ return
+ else:
+ location = self._mgr.get_textures_page(bo)
+ bitmap = self._mgr.add_object(plMipmap, name=name, loc=location)
+ korlib.generate_mipmap(texture, bitmap)
+
+ # Store the created plBitmap and toss onto the layer
+ self._hsbitmaps[name] = bitmap
+ layer.texture = bitmap.key
+
+ @property
+ def _mgr(self):
+ return self._exporter().mgr
+
+ def _propagate_material_settings(self, bm, layer):
+ """Converts settings from the Blender Material to corresponding plLayer settings"""
+ state = layer.state
+
+ # Shade Flags
+ if not bm.use_mist:
+ state.shadeFlags |= hsGMatState.kShadeNoFog # Dead in CWE
+ state.shadeFlags |= hsGMatState.kShadeReallyNoFog
+
+ # Colors
+ layer.ambient = utils.color(bpy.context.scene.world.ambient_color)
+ layer.preshade = utils.color(bm.diffuse_color)
+ layer.runtime = utils.color(bm.diffuse_color)
+ layer.specular = utils.color(bm.specular_color)
diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py
index dd864bb..db2176c 100644
--- a/korman/exporter/mesh.py
+++ b/korman/exporter/mesh.py
@@ -15,8 +15,10 @@
import bpy
from PyHSPlasma import *
+import weakref
from . import explosions
+from . import material
from . import utils
_MAX_VERTS_PER_SPAN = 0xFFFF
@@ -32,9 +34,14 @@ class _RenderLevel:
_MAJOR_SHIFT = 28
_MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1)
- def __init__(self):
+ def __init__(self, hsgmat, pass_index):
+ # TODO: Use hsGMaterial to determine major and minor
self.level = 0
+ # We use the blender material's pass index (which we stashed in the hsGMaterial) to increment
+ # the render pass, just like it says...
+ self.level += pass_index
+
def __hash__(self):
return hash(self.level)
@@ -52,11 +59,11 @@ class _RenderLevel:
class _DrawableCriteria:
- def __init__(self, hsgmat):
+ def __init__(self, hsgmat, pass_index):
_layer = hsgmat.layers[0].object # better doggone well have a layer...
self.blend_span = bool(_layer.state.blendFlags & hsGMatState.kBlendMask)
self.criteria = 0 # TODO
- self.render_level = _RenderLevel()
+ self.render_level = _RenderLevel(hsgmat, pass_index)
def __eq__(self, other):
if not isinstance(other, _DrawableCriteria):
@@ -81,14 +88,22 @@ class MeshConverter:
_dspans = {}
_mesh_geospans = {}
- def __init__(self, mgr):
- self._mgr = mgr
+ def __init__(self, exporter):
+ self._exporter = weakref.ref(exporter)
+ self.material = material.MaterialConverter(exporter)
- def _create_geospan(self, bo, bm, hsgmat):
+ def _create_geospan(self, bo, mesh, bm, hsgmat):
"""Initializes a plGeometrySpan from a Blender Object and an hsGMaterial"""
geospan = plGeometrySpan()
geospan.material = hsgmat
+ # GeometrySpan format
+ # For now, we really only care about the number of UVW Channels
+ numUVWchans = len(mesh.tessface_uv_textures)
+ if numUVWchans > plGeometrySpan.kUVCountMask:
+ raise explosions.TooManyUVChannelsError(bo, bm)
+ geospan.format = numUVWchans
+
# TODO: Props
# TODO: RunTime lights (requires libHSPlasma feature)
@@ -107,7 +122,7 @@ class MeshConverter:
for loc in self._dspans.values():
for dspan in loc.values():
- print("\tFinalizing DSpan: '{}'".format(dspan.key.name))
+ print(" Finalizing DSpan: '{}'".format(dspan.key.name))
# This mega-function does a lot:
# 1. Converts SourceSpans (geospans) to Icicles and bakes geometry into plGBuffers
@@ -117,46 +132,59 @@ class MeshConverter:
dspan.composeGeometry(True, True)
def _export_geometry(self, mesh, geospans):
- geodata = [None] * len(mesh.materials)
- geoverts = [None] * len(mesh.vertices)
- for i, garbage in enumerate(geodata):
- geodata[i] = {
- "blender2gs": [None] * len(mesh.vertices),
- "triangles": [],
- "vertices": [],
- }
-
- # Go ahead and naively convert all vertices into TempVertices for the GeoSpans
- for i, source in enumerate(mesh.vertices):
- vertex = plGeometrySpan.TempVertex()
- vertex.color = hsColor32(red=255, green=0, blue=0, alpha=255) # FIXME trollface.jpg testing hacks
- vertex.normal = utils.vector3(source.normal)
- vertex.position = utils.vector3(source.co)
- geoverts[i] = vertex
+ _geodatacls = type("_GeoData",
+ (object,),
+ {
+ "blender2gs": [{} for i in mesh.vertices],
+ "triangles": [],
+ "vertices": []
+ })
+ geodata = [_geodatacls() for i in mesh.materials]
# Convert Blender faces into things we can stuff into libHSPlasma
- for tessface in mesh.tessfaces:
+ for i, tessface in enumerate(mesh.tessfaces):
data = geodata[tessface.material_index]
face_verts = []
# Convert to per-material indices
- for i in tessface.vertices:
- if data["blender2gs"][i] is None:
- data["blender2gs"][i] = len(data["vertices"])
- data["vertices"].append(geoverts[i])
- face_verts.append(data["blender2gs"][i])
+ for j in tessface.vertices:
+ # Unpack the UV coordinates from each UV Texture layer
+ uvws = []
+ for uvtex in mesh.tessface_uv_textures:
+ uv = getattr(uvtex.data[i], "uv{}".format(j+1))
+ # In Blender, UVs have no Z coordinate
+ uvws.append((uv.x, uv.y))
+
+ # Grab VCols (TODO--defaulting to white for now)
+ # This will be finalized once the vertex color light code baking is in
+ color = (0, 0, 0, 255)
+
+ # Now, we'll index into the vertex dict using the per-face elements :(
+ # We're using tuples because lists are not hashable. The many mathutils and PyHSPlasma
+ # types are not either, and it's entirely too much work to fool with all that.
+ coluv = (color, tuple(uvws))
+ if coluv not in data.blender2gs[j]:
+ source = mesh.vertices[j]
+ vertex = plGeometrySpan.TempVertex()
+ vertex.position = utils.vector3(source.co)
+ vertex.normal = utils.vector3(source.normal)
+ vertex.color = hsColor32(*color)
+ vertex.uvs = [hsVector3(uv[0], 1.0-uv[1], 0.0) for uv in uvws]
+ data.blender2gs[j][coluv] = len(data.vertices)
+ data.vertices.append(vertex)
+ face_verts.append(data.blender2gs[j][coluv])
# Convert to triangles, if need be...
if len(face_verts) == 3:
- data["triangles"] += face_verts
+ data.triangles += face_verts
elif len(face_verts) == 4:
- data["triangles"] += (face_verts[0], face_verts[1], face_verts[2])
- data["triangles"] += (face_verts[0], face_verts[2], face_verts[3])
+ data.triangles += (face_verts[0], face_verts[1], face_verts[2])
+ data.triangles += (face_verts[0], face_verts[2], face_verts[3])
# Time to finish it up...
for i, data in enumerate(geodata):
- geospan = geospans[i]
- numVerts = len(data["vertices"])
+ geospan = geospans[i][0]
+ numVerts = len(data.vertices)
# Soft vertex limit at 0x8000 for PotS and below. Works fine as long as it's a uint16
# MOUL only allows signed int16s, however :/
@@ -166,8 +194,8 @@ class MeshConverter:
pass # FIXME
# If we're still here, let's add our data to the GeometrySpan
- geospan.indices = data["triangles"]
- geospan.vertices = data["vertices"]
+ geospan.indices = data.triangles
+ geospan.vertices = data.vertices
def export_object(self, bo):
# Have we already exported this mesh?
@@ -195,9 +223,9 @@ class MeshConverter:
# Step 3: Add plGeometrySpans to the appropriate DSpan and create indices
_diindices = {}
- for geospan in geospans:
- dspan = self._find_create_dspan(bo, geospan.material.object)
- print("\tExported hsGMaterial '{}' geometry into '{}'".format(geospan.material.name, dspan.key.name))
+ 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))
idx = dspan.addSourceSpan(geospan)
if dspan not in _diindices:
_diindices[dspan] = [idx,]
@@ -213,25 +241,15 @@ class MeshConverter:
drawables.append((dspan.key, idx))
return drawables
- def _export_material(self, bo, bm):
- """Exports a single Material Slot as an hsGMaterial"""
- # FIXME HACKS
- hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo)
- fake_layer = self._mgr.add_object(plLayer, name="{}_AutoLayer".format(bm.name), bl=bo)
- hsgmat.addLayer(fake_layer.key)
- # ...
-
- return hsgmat.key
-
def _export_material_spans(self, bo, mesh):
"""Exports all Materials and creates plGeometrySpans"""
geospans = [None] * len(mesh.materials)
for i, blmat in enumerate(mesh.materials):
- hsgmat = self._export_material(bo, blmat)
- geospans[i] = self._create_geospan(bo, blmat, hsgmat)
+ hsgmat = self.material.export_material(bo, blmat)
+ geospans[i] = (self._create_geospan(bo, mesh, blmat, hsgmat), blmat.pass_index)
return geospans
- def _find_create_dspan(self, bo, hsgmat):
+ def _find_create_dspan(self, bo, hsgmat, pass_index):
location = self._mgr.get_location(bo)
if location not in self._dspans:
self._dspans[location] = {}
@@ -241,8 +259,7 @@ class MeshConverter:
# [... document me ...]
# We're using pass index to do just what it was designed for. Cyan has a nicer "depends on"
# draw component, but pass index is the Blender way, so that's what we're doing.
- crit = _DrawableCriteria(hsgmat)
- crit.render_level.level += bo.pass_index
+ crit = _DrawableCriteria(hsgmat, pass_index)
if crit not in self._dspans[location]:
# AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans
@@ -250,8 +267,17 @@ class MeshConverter:
node = self._mgr.get_scene_node(location)
name = "{}_{:08X}_{:X}{}".format(node.name, crit.render_level.level, crit.criteria, crit.span_type)
dspan = self._mgr.add_object(pl=plDrawableSpans, name=name, loc=location)
+
+ dspan.criteria = crit.criteria
+ # TODO: props
+ dspan.renderLevel = crit.render_level.level
dspan.sceneNode = node # AddViaNotify
+
self._dspans[location][crit] = dspan
return dspan
else:
return self._dspans[location][crit]
+
+ @property
+ def _mgr(self):
+ return self._exporter().mgr
diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py
index 8539ade..8e9201c 100644
--- a/korman/exporter/utils.py
+++ b/korman/exporter/utils.py
@@ -15,6 +15,10 @@
from PyHSPlasma import *
+def color(blcolor, alpha=1.0):
+ """Converts a Blender Color into an hsColorRGBA"""
+ return hsColorRGBA(blcolor.r, blcolor.g, blcolor.b, alpha)
+
def matrix44(blmat):
"""Converts a mathutils.Matrix to an hsMatrix44"""
hsmat = hsMatrix44()
diff --git a/korman/render.py b/korman/render.py
index bb3ea6a..ac16ca0 100644
--- a/korman/render.py
+++ b/korman/render.py
@@ -31,6 +31,12 @@ properties_material.MATERIAL_PT_options.COMPAT_ENGINES.add("PLASMA_GAME")
properties_material.MATERIAL_PT_preview.COMPAT_ENGINES.add("PLASMA_GAME")
del properties_material
+from bl_ui import properties_texture
+for i in dir(properties_texture):
+ attr = getattr(properties_texture, i)
+ if hasattr(attr, "COMPAT_ENGINES"):
+ getattr(attr, "COMPAT_ENGINES").add("PLASMA_GAME")
+del properties_texture
@classmethod
def _new_poll(cls, context):