From 32dcac54f5791d3a8be7ced818678883ebd4ff3a Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 25 Jun 2014 19:25:52 -0400 Subject: [PATCH] Rudimentary material exporter --- .gitignore | 3 + korlib/CMakeLists.txt | 30 +++++ korlib/generate_mipmap.cpp | 213 ++++++++++++++++++++++++++++++++++ korlib/module.cpp | 48 ++++++++ korlib/pyMipmap.h | 27 +++++ korlib/utils.hpp | 89 ++++++++++++++ korman/exporter/convert.py | 11 +- korman/exporter/explosions.py | 12 ++ korman/exporter/manager.py | 4 +- korman/exporter/material.py | 130 +++++++++++++++++++++ korman/exporter/mesh.py | 134 ++++++++++++--------- korman/exporter/utils.py | 4 + korman/render.py | 6 + 13 files changed, 651 insertions(+), 60 deletions(-) create mode 100644 korlib/CMakeLists.txt create mode 100644 korlib/generate_mipmap.cpp create mode 100644 korlib/module.cpp create mode 100644 korlib/pyMipmap.h create mode 100644 korlib/utils.hpp create mode 100644 korman/exporter/material.py 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):