Browse Source

Merge pull request #125 from Hoikas/envmap

Environment Map Improvements
pull/133/head
Adam Johnson 6 years ago committed by GitHub
parent
commit
070db331fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      korlib/korlib.h
  2. 1
      korlib/module.cpp
  3. 417
      korlib/texture.cpp
  4. 2
      korlib/texture.h
  5. 4
      korman/exporter/convert.py
  6. 45
      korman/exporter/image.py
  7. 281
      korman/exporter/material.py
  8. 2
      korman/helpers.py
  9. 221
      korman/korlib/texture.py
  10. 1
      korman/operators/__init__.py
  11. 7
      korman/operators/op_export.py
  12. 237
      korman/operators/op_image.py
  13. 19
      korman/operators/op_toolbox.py
  14. 9
      korman/ui/ui_texture.py
  15. 6
      korman/ui/ui_toolbox.py
  16. 1
      korman/ui/ui_world.py

2
korlib/korlib.h

@ -23,11 +23,13 @@
#include <Python.h> #include <Python.h>
#define _pycs(x) const_cast<char*>(x) #define _pycs(x) const_cast<char*>(x)
#define arrsize(a) (sizeof(a) / sizeof((a)[0]))
class PyObjectRef { class PyObjectRef {
PyObject* m_object; PyObject* m_object;
public: public:
PyObjectRef() : m_object() { }
PyObjectRef(PyObject* o) : m_object(o) { } PyObjectRef(PyObject* o) : m_object(o) { }
~PyObjectRef() { Py_XDECREF(m_object); } ~PyObjectRef() { Py_XDECREF(m_object); }

1
korlib/module.cpp

@ -23,6 +23,7 @@ extern "C" {
static PyMethodDef korlib_Methods[] = { static PyMethodDef korlib_Methods[] = {
{ _pycs("create_bump_LUT"), (PyCFunction)create_bump_LUT, METH_VARARGS, NULL }, { _pycs("create_bump_LUT"), (PyCFunction)create_bump_LUT, METH_VARARGS, NULL },
{ _pycs("inspect_vorbisfile"), (PyCFunction)inspect_vorbisfile, METH_VARARGS, NULL }, { _pycs("inspect_vorbisfile"), (PyCFunction)inspect_vorbisfile, METH_VARARGS, NULL },
{ _pycs("scale_image"), (PyCFunction)scale_image, METH_KEYWORDS | METH_VARARGS, NULL },
{ NULL, NULL, 0, NULL }, { NULL, NULL, 0, NULL },
}; };

417
korlib/texture.cpp

@ -26,22 +26,167 @@
#include <GL/gl.h> #include <GL/gl.h>
#include <PRP/Surface/plMipmap.h> #include <PRP/Surface/plMipmap.h>
#ifndef GL_GENERATE_MIPMAP
# define GL_GENERATE_MIPMAP 0x8191
#endif // GL_GENERATE_MIPMAP
#define TEXTARGET_TEXTURE_2D 0 #define TEXTARGET_TEXTURE_2D 0
// ===============================================================================================
static inline void _ensure_copy_bytes(PyObject* parent, PyObject*& data) {
// PyBytes objects are immutable and ought not to be changed once they are returned to Python
// code. Therefore, this tests to see if the given bytes object is the same as one we're holding.
// If so, a new copy is constructed seamlessly.
if (parent == data) {
Py_ssize_t size;
char* buf;
PyBytes_AsStringAndSize(parent, &buf, &size);
data = PyBytes_FromStringAndSize(buf, size);
Py_DECREF(parent);
}
}
template<typename T>
static T _ensure_power_of_two(T value) {
return static_cast<T>(std::pow(2, std::floor(std::log2(value))));
}
static void _flip_image(size_t width, size_t dataSize, uint8_t* data) {
// OpenGL returns a flipped image, so we must reflip it.
size_t row_stride = width * 4;
uint8_t* sptr = data;
uint8_t* eptr = data + (dataSize - row_stride);
uint8_t* temp = new uint8_t[row_stride];
do {
memcpy(temp, sptr, row_stride);
memcpy(sptr, eptr, row_stride);
memcpy(eptr, temp, row_stride);
} while ((sptr += row_stride) < (eptr -= row_stride));
delete[] temp;
}
static inline bool _get_float(PyObject* source, const char* attr, float& result) { static inline bool _get_float(PyObject* source, const char* attr, float& result) {
if (source) {
PyObjectRef pyfloat = PyObject_GetAttrString(source, attr); PyObjectRef pyfloat = PyObject_GetAttrString(source, attr);
if (pyfloat) { if (pyfloat) {
result = (float)PyFloat_AsDouble(pyfloat); result = (float)PyFloat_AsDouble(pyfloat);
return PyErr_Occurred() == NULL; return PyErr_Occurred() == NULL;
} }
}
return false; return false;
} }
extern "C" { static inline int _get_num_levels(size_t width, size_t height) {
int num_levels = (int)std::floor(std::log2(std::max((float)width, (float)height))) + 1;
// Major Workaround Ahoy
// There is a bug in Cyan's level size algorithm that causes it to not allocate enough memory
// for the color block in certain mipmaps. I personally have encountered an access violation on
// 1x1 DXT5 mip levels -- the code only allocates an alpha block and not a color block. Paradox
// reports that if any dimension is smaller than 4px in a mip level, OpenGL doesn't like Cyan generated
// data. So, we're going to lop off the last two mip levels, which should be 1px and 2px as the smallest.
// This bug is basically unfixable without crazy hacks because of the way Plasma reads in texture data.
// "<Deledrius> I feel like any texture at a 1x1 level is essentially academic. I mean, JPEG/DXT
// doesn't even compress that, and what is it? Just the average color of the whole
// texture in a single pixel?"
// :)
return std::max(num_levels - 2, 2);
}
static void _scale_image(const uint8_t* srcBuf, const size_t srcW, const size_t srcH,
uint8_t* dstBuf, const size_t dstW, const size_t dstH) {
float scaleX = static_cast<float>(srcW) / static_cast<float>(dstW);
float scaleY = static_cast<float>(srcH) / static_cast<float>(dstH);
float filterW = std::max(scaleX, 1.f);
float filterH = std::max(scaleY, 1.f);
size_t srcRowspan = srcW * sizeof(uint32_t);
size_t dstIdx = 0;
for (size_t dstY = 0; dstY < dstH; ++dstY) {
float srcY = dstY * scaleY;
ssize_t srcY_start = std::max(static_cast<ssize_t>(srcY - filterH),
static_cast<ssize_t>(0));
ssize_t srcY_end = std::min(static_cast<ssize_t>(srcY + filterH),
static_cast<ssize_t>(srcH - 1));
float weightsY[16];
for (ssize_t i = srcY_start; i <= srcY_end && i - srcY_start < arrsize(weightsY); ++i)
weightsY[i - srcY_start] = 1.f - std::abs((i - srcY) / filterH);
for (size_t dstX = 0; dstX < dstW; ++dstX) {
float srcX = dstX * scaleX;
ssize_t srcX_start = std::max(static_cast<ssize_t>(srcX - filterW),
static_cast<ssize_t>(0));
ssize_t srcX_end = std::min(static_cast<ssize_t>(srcX + filterW),
static_cast<ssize_t>(srcW - 1));
float weightsX[16];
for (ssize_t i = srcX_start; i <= srcX_end && i - srcX_start < arrsize(weightsX); ++i)
weightsX[i - srcX_start] = 1.f - std::abs((i - srcX) / filterW);
float accum_color[] = { 0.f, 0.f, 0.f, 0.f };
float weight_total = 0.f;
for (size_t i = srcY_start; i <= srcY_end; ++i) {
float weightY;
if (i - srcY_start < arrsize(weightsY))
weightY = weightsY[i - srcY_start];
else
weightY = 1.f - std::abs((i - srcY) / filterH);
if (weightY <= 0.f)
continue;
size_t srcIdx = ((i * srcRowspan) + (srcX_start * sizeof(uint32_t)));
for (size_t j = srcX_start; j <= srcX_end; ++j, srcIdx += sizeof(uint32_t)) {
float weightX;
if (j - srcX_start < arrsize(weightsX))
weightX = weightsX[j - srcX_start];
else
weightX = 1.f - std::abs((j - srcX) / filterW);
float weight = weightX * weightY;
if (weight > 0.f) {
for (size_t k = 0; k < sizeof(uint32_t); ++k)
accum_color[k] += (static_cast<float>(srcBuf[srcIdx+k]) / 255.f) * weight;
weight_total += weight;
}
}
}
for (size_t k = 0; k < sizeof(uint32_t); ++k)
accum_color[k] *= 1.f / weight_total;
// Whew.
for (size_t k = 0; k < sizeof(uint32_t); ++k)
dstBuf[dstIdx+k] = static_cast<uint8_t>(accum_color[k] * 255.f);
dstIdx += sizeof(uint32_t);
}
}
}
// ===============================================================================================
PyObject* scale_image(PyObject*, PyObject* args, PyObject* kwargs) {
static char* kwlist[] = { _pycs("buf"), _pycs("srcW"), _pycs("srcH"),
_pycs("dstW"), _pycs("dstH"), NULL };
const uint8_t* srcBuf;
int srcBufSz;
uint32_t srcW, srcH, dstW, dstH;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y#IIII", kwlist, &srcBuf, &srcBufSz, &srcW, &srcH, &dstW, &dstH)) {
PyErr_SetString(PyExc_TypeError, "scale_image expects a bytes object, int, int, int int");
return NULL;
}
int expectedBufSz = srcW * srcH * sizeof(uint32_t);
if (srcBufSz != expectedBufSz) {
PyErr_Format(PyExc_ValueError, "buf size (%i bytes) incorrect (expected: %i bytes)", srcBufSz, expectedBufSz);
return NULL;
}
PyObject* dst = PyBytes_FromStringAndSize(NULL, dstW * dstH * sizeof(uint32_t));
uint8_t* dstBuf = reinterpret_cast<uint8_t*>(PyBytes_AS_STRING(dst));
_scale_image(srcBuf, srcW, srcH, dstBuf, dstW, dstH);
return dst;
}
// ===============================================================================================
enum { enum {
TEX_DETAIL_ALPHA = 0, TEX_DETAIL_ALPHA = 0,
@ -53,10 +198,11 @@ typedef struct {
PyObject_HEAD PyObject_HEAD
PyObject* m_blenderImage; PyObject* m_blenderImage;
PyObject* m_textureKey; PyObject* m_textureKey;
bool m_ownIt; PyObject* m_imageData;
GLint m_prevImage; GLint m_width;
bool m_changedState; GLint m_height;
GLint m_mipmapState; bool m_bgra;
bool m_imageInverted;
} pyGLTexture; } pyGLTexture;
typedef struct { typedef struct {
@ -65,9 +211,12 @@ typedef struct {
bool fPyOwned; bool fPyOwned;
} pyMipmap; } pyMipmap;
// ===============================================================================================
static void pyGLTexture_dealloc(pyGLTexture* self) { static void pyGLTexture_dealloc(pyGLTexture* self) {
Py_XDECREF(self->m_textureKey); Py_CLEAR(self->m_textureKey);
Py_XDECREF(self->m_blenderImage); Py_CLEAR(self->m_blenderImage);
Py_CLEAR(self->m_imageData);
Py_TYPE(self)->tp_free((PyObject*)self); Py_TYPE(self)->tp_free((PyObject*)self);
} }
@ -75,26 +224,36 @@ static PyObject* pyGLTexture_new(PyTypeObject* type, PyObject* args, PyObject* k
pyGLTexture* self = (pyGLTexture*)type->tp_alloc(type, 0); pyGLTexture* self = (pyGLTexture*)type->tp_alloc(type, 0);
self->m_blenderImage = NULL; self->m_blenderImage = NULL;
self->m_textureKey = NULL; self->m_textureKey = NULL;
self->m_ownIt = false; self->m_imageData = NULL;
self->m_prevImage = 0; self->m_width = 0;
self->m_changedState = false; self->m_height = 0;
self->m_mipmapState = 0; self->m_bgra = false;
self->m_imageInverted = false;
return (PyObject*)self; return (PyObject*)self;
} }
static int pyGLTexture___init__(pyGLTexture* self, PyObject* args, PyObject* kwds) { static int pyGLTexture___init__(pyGLTexture* self, PyObject* args, PyObject* kwds) {
if (!PyArg_ParseTuple(args, "O", &self->m_textureKey)) { static char* kwlist[] = { _pycs("texkey"), _pycs("image"), _pycs("bgra"), _pycs("fast"), NULL };
PyErr_SetString(PyExc_TypeError, "expected a korman.exporter.material._Texture"); if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OObb", kwlist, &self->m_textureKey, &self->m_blenderImage,
&self->m_bgra, &self->m_imageInverted)) {
PyErr_SetString(PyExc_TypeError, "expected a korman.exporter.material._Texture or a bpy.types.Image");
return -1; return -1;
} }
if (!self->m_blenderImage && !self->m_textureKey) {
PyErr_SetString(PyExc_TypeError, "expected a korman.exporter.material._Texture or a bpy.types.Image");
return -1;
}
Py_XINCREF(self->m_blenderImage);
Py_XINCREF(self->m_textureKey);
if (!self->m_blenderImage) {
self->m_blenderImage = PyObject_GetAttrString(self->m_textureKey, "image"); self->m_blenderImage = PyObject_GetAttrString(self->m_textureKey, "image");
}
if (!self->m_blenderImage) { if (!self->m_blenderImage) {
PyErr_SetString(PyExc_RuntimeError, "Could not fetch Blender Image"); PyErr_SetString(PyExc_RuntimeError, "Could not fetch Blender Image");
return -1; return -1;
} }
Py_INCREF(self->m_textureKey);
// Done! // Done!
return 0; return 0;
} }
@ -115,12 +274,13 @@ static PyObject* pyGLTexture__enter__(pyGLTexture* self) {
return NULL; return NULL;
} }
glGetIntegerv(GL_TEXTURE_BINDING_2D, &self->m_prevImage); GLint prevImage;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevImage);
GLuint image_bindcode = PyLong_AsUnsignedLong(bindcode); GLuint image_bindcode = PyLong_AsUnsignedLong(bindcode);
self->m_ownIt = image_bindcode == 0; bool ownit = image_bindcode == 0;
// Load image into GL // Load image into GL
if (self->m_ownIt) { if (ownit) {
PyObjectRef new_bind = PyObject_CallMethod(self->m_blenderImage, "gl_load", NULL); PyObjectRef new_bind = PyObject_CallMethod(self->m_blenderImage, "gl_load", NULL);
if (!PyLong_Check(new_bind)) { if (!PyLong_Check(new_bind)) {
PyErr_SetString(PyExc_TypeError, "gl_load() did not return a long"); PyErr_SetString(PyExc_TypeError, "gl_load() did not return a long");
@ -144,64 +304,39 @@ static PyObject* pyGLTexture__enter__(pyGLTexture* self) {
} }
// Set image as current in GL // Set image as current in GL
if (self->m_prevImage != image_bindcode) { bool changedState = prevImage != image_bindcode;
self->m_changedState = true; if (changedState)
glBindTexture(GL_TEXTURE_2D, image_bindcode); glBindTexture(GL_TEXTURE_2D, image_bindcode);
}
// Misc GL state // Now we can load the image data...
glGetTexParameteriv(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, &self->m_mipmapState); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &self->m_width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &self->m_height);
size_t bufsz = self->m_width * self->m_height * sizeof(uint32_t);
self->m_imageData = PyBytes_FromStringAndSize(NULL, bufsz);
char* imbuf = PyBytes_AS_STRING(self->m_imageData);
GLint fmt = self->m_bgra ? GL_BGRA_EXT : GL_RGBA;
glGetTexImage(GL_TEXTURE_2D, 0, fmt, GL_UNSIGNED_BYTE, reinterpret_cast<GLvoid*>(imbuf));
// OpenGL returns image data flipped upside down. We'll flip it to be correct, if requested.
if (!self->m_imageInverted)
_flip_image(self->m_width, bufsz, reinterpret_cast<uint8_t*>(imbuf));
// If we had to play with ourse^H^H^H^H^Hblender's image state, let's reset it
if (changedState)
glBindTexture(GL_TEXTURE_2D, prevImage);
if (ownit)
PyObjectRef result = PyObject_CallMethod(self->m_blenderImage, "gl_free", NULL);
Py_INCREF(self); Py_INCREF(self);
return (PyObject*)self; return (PyObject*)self;
} }
static PyObject* pyGLTexture__exit__(pyGLTexture* self, PyObject*) { static PyObject* pyGLTexture__exit__(pyGLTexture* self, PyObject*) {
// We don't care about the args here Py_CLEAR(self->m_imageData);
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, self->m_mipmapState);
if (self->m_changedState)
glBindTexture(GL_TEXTURE_2D, self->m_prevImage);
Py_RETURN_NONE;
}
static PyObject* pyGLTexture_generate_mipmap(pyGLTexture* self) {
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, 1);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
struct _LevelData
{
GLint m_width;
GLint m_height;
uint8_t* m_data;
size_t m_dataSize;
_LevelData(GLint w, GLint h, uint8_t* ptr, size_t sz)
: m_width(w), m_height(h), m_data(ptr), m_dataSize(sz)
{ }
};
static inline int _get_num_levels(pyGLTexture* self) {
PyObjectRef size = PyObject_GetAttrString(self->m_blenderImage, "size");
float width = (float)PyFloat_AsDouble(PySequence_GetItem(size, 0));
float height = (float)PyFloat_AsDouble(PySequence_GetItem(size, 1));
int num_levels = (int)std::floor(std::log2(std::max(width, height))) + 1;
// Major Workaround Ahoy
// There is a bug in Cyan's level size algorithm that causes it to not allocate enough memory
// for the color block in certain mipmaps. I personally have encountered an access violation on
// 1x1 DXT5 mip levels -- the code only allocates an alpha block and not a color block. Paradox
// reports that if any dimension is smaller than 4px in a mip level, OpenGL doesn't like Cyan generated
// data. So, we're going to lop off the last two mip levels, which should be 1px and 2px as the smallest.
// This bug is basically unfixable without crazy hacks because of the way Plasma reads in texture data.
// "<Deledrius> I feel like any texture at a 1x1 level is essentially academic. I mean, JPEG/DXT
// doesn't even compress that, and what is it? Just the average color of the whole
// texture in a single pixel?"
// :)
return std::max(num_levels - 2, 2);
}
static int _generate_detail_alpha(pyGLTexture* self, GLint level, float* result) { static int _generate_detail_alpha(pyGLTexture* self, GLint level, float* result) {
float dropoff_start, dropoff_stop, detail_max, detail_min; float dropoff_start, dropoff_stop, detail_max, detail_min;
if (!_get_float(self->m_textureKey, "detail_fade_start", dropoff_start)) if (!_get_float(self->m_textureKey, "detail_fade_start", dropoff_start))
@ -214,9 +349,9 @@ static int _generate_detail_alpha(pyGLTexture* self, GLint level, float* result)
return -1; return -1;
dropoff_start /= 100.f; dropoff_start /= 100.f;
dropoff_start *= _get_num_levels(self); dropoff_start *= _get_num_levels(self->m_width, self->m_height);
dropoff_stop /= 100.f; dropoff_stop /= 100.f;
dropoff_stop *= _get_num_levels(self); dropoff_stop *= _get_num_levels(self->m_width, self->m_height);
detail_max /= 100.f; detail_max /= 100.f;
detail_min /= 100.f; detail_min /= 100.f;
@ -232,7 +367,9 @@ static int _generate_detail_map(pyGLTexture* self, uint8_t* buf, size_t bufsz, G
float alpha; float alpha;
if (_generate_detail_alpha(self, level, &alpha) != 0) if (_generate_detail_alpha(self, level, &alpha) != 0)
return -1; return -1;
PyObjectRef pydetail_blend = PyObject_GetAttrString(self->m_textureKey, "detail_blend"); PyObjectRef pydetail_blend;
if (self->m_textureKey)
pydetail_blend = PyObject_GetAttrString(self->m_textureKey, "detail_blend");
if (!pydetail_blend) if (!pydetail_blend)
return -1; return -1;
@ -265,102 +402,133 @@ static int _generate_detail_map(pyGLTexture* self, uint8_t* buf, size_t bufsz, G
return 0; return 0;
} }
static _LevelData _get_level_data(pyGLTexture* self, GLint level, bool bgra, PyObject* report) { static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, PyObject* kwargs) {
GLint width, height; static char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("report"),
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width); _pycs("indent"), _pycs("fast"), NULL };
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height); GLint level = 0;
GLenum fmt = bgra ? GL_BGRA_EXT : GL_RGBA; bool calc_alpha = false;
PyObject* report = nullptr;
int indent = 2;
bool fast = false;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ibOib", kwlist, &level, &calc_alpha, &report, &indent, &fast)) {
PyErr_SetString(PyExc_TypeError, "get_level_data expects an optional int, bool, obejct, int, bool");
return NULL;
}
// We only ever want to return POT images for use in Plasma
auto eWidth = _ensure_power_of_two(self->m_width) >> level;
auto eHeight = _ensure_power_of_two(self->m_height) >> level;
bool is_og = eWidth == self->m_width && eHeight == self->m_height;
size_t bufsz = eWidth * eHeight * sizeof(uint32_t);
// Print out the debug message // Print out the debug message
if (report && report != Py_None) { if (report && report != Py_None) {
PyObjectRef msg_func = PyObject_GetAttrString(report, "msg"); PyObjectRef msg_func = PyObject_GetAttrString(report, "msg");
PyObjectRef args = Py_BuildValue("siii", "Level #{}: {}x{}", level, width, height); PyObjectRef args = Py_BuildValue("siii", "Level #{}: {}x{}", level, eWidth, eHeight);
PyObjectRef kwargs = Py_BuildValue("{s:i}", "indent", 2); PyObjectRef kwargs = Py_BuildValue("{s:i}", "indent", indent);
PyObjectRef result = PyObject_Call(msg_func, args, kwargs); PyObjectRef result = PyObject_Call(msg_func, args, kwargs);
} }
size_t bufsz; PyObject* data;
bufsz = (width * height * 4); if (is_og) {
uint8_t* buf = new uint8_t[bufsz]; Py_INCREF(self->m_imageData);
glGetTexImage(GL_TEXTURE_2D, level, fmt, GL_UNSIGNED_BYTE, reinterpret_cast<GLvoid*>(buf)); data = self->m_imageData;
return _LevelData(width, height, buf, bufsz); } else {
data = PyBytes_FromStringAndSize(NULL, bufsz);
uint8_t* dstBuf = reinterpret_cast<uint8_t*>(PyBytes_AsString(data)); // AS_STRING :(
uint8_t* srcBuf = reinterpret_cast<uint8_t*>(PyBytes_AsString(self->m_imageData));
_scale_image(srcBuf, self->m_width, self->m_height, dstBuf, eWidth, eHeight);
} }
static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, PyObject* kwargs) { // Make sure the level data is not flipped upside down...
static char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("bgra"), if (self->m_imageInverted && !fast) {
_pycs("report"), _pycs("fast"), NULL }; _ensure_copy_bytes(self->m_blenderImage, data);
GLint level = 0; _flip_image(eWidth, bufsz, reinterpret_cast<uint8_t*>(PyBytes_AS_STRING(data)));
bool calc_alpha = false;
bool bgra = false;
PyObject* report = nullptr;
bool fast = false;
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, report);
if (fast)
return PyBytes_FromStringAndSize((char*)data.m_data, data.m_dataSize);
// OpenGL returns a flipped image, so we must reflip it.
size_t row_stride = data.m_width * 4;
uint8_t* sptr = data.m_data;
uint8_t* eptr = data.m_data + (data.m_dataSize - row_stride);
uint8_t* temp = new uint8_t[row_stride];
do {
memcpy(temp, sptr, row_stride);
memcpy(sptr, eptr, row_stride);
memcpy(eptr, temp, row_stride);
} while ((sptr += row_stride) < (eptr -= row_stride));
delete[] temp;
// Detail blend // Detail blend
if (self->m_textureKey) {
PyObjectRef is_detail_map = PyObject_GetAttrString(self->m_textureKey, "is_detail_map"); PyObjectRef is_detail_map = PyObject_GetAttrString(self->m_textureKey, "is_detail_map");
if (PyLong_AsLong(is_detail_map) != 0) { if (PyLong_AsLong(is_detail_map) != 0) {
if (_generate_detail_map(self, data.m_data, data.m_dataSize, level) != 0) { _ensure_copy_bytes(self->m_imageData, data);
delete[] data.m_data; uint8_t* buf = reinterpret_cast<uint8_t*>(PyBytes_AS_STRING(data));
if (_generate_detail_map(self, buf, bufsz, level) != 0) {
PyErr_SetString(PyExc_RuntimeError, "error while baking detail map"); PyErr_SetString(PyExc_RuntimeError, "error while baking detail map");
Py_DECREF(data);
return NULL; return NULL;
} }
} }
}
if (calc_alpha) { if (calc_alpha) {
for (size_t i = 0; i < data.m_dataSize; i += 4) _ensure_copy_bytes(self->m_imageData, data);
data.m_data[i + 3] = (data.m_data[i + 0] + data.m_data[i + 1] + data.m_data[i + 2]) / 3; char* buf = PyBytes_AS_STRING(data);
for (size_t i = 0; i < bufsz; i += 4)
buf[i + 3] = (buf[i + 0] + buf[i + 1] + buf[i + 2]) / 3;
} }
return PyBytes_FromStringAndSize((char*)data.m_data, data.m_dataSize); return data;
} }
static PyMethodDef pyGLTexture_Methods[] = { static PyMethodDef pyGLTexture_Methods[] = {
{ _pycs("__enter__"), (PyCFunction)pyGLTexture__enter__, METH_NOARGS, NULL }, { _pycs("__enter__"), (PyCFunction)pyGLTexture__enter__, METH_NOARGS, NULL },
{ _pycs("__exit__"), (PyCFunction)pyGLTexture__exit__, METH_VARARGS, NULL }, { _pycs("__exit__"), (PyCFunction)pyGLTexture__exit__, METH_VARARGS, NULL },
{ _pycs("generate_mipmap"), (PyCFunction)pyGLTexture_generate_mipmap, METH_NOARGS, NULL },
{ _pycs("get_level_data"), (PyCFunction)pyGLTexture_get_level_data, METH_KEYWORDS | METH_VARARGS, NULL }, { _pycs("get_level_data"), (PyCFunction)pyGLTexture_get_level_data, METH_KEYWORDS | METH_VARARGS, NULL },
{ NULL, NULL, 0, NULL } { NULL, NULL, 0, NULL }
}; };
static PyObject* pyGLTexture_get_has_alpha(pyGLTexture* self, void*) { static PyObject* pyGLTexture_get_has_alpha(pyGLTexture* self, void*) {
_LevelData data = _get_level_data(self, 0, false, nullptr); char* data = PyBytes_AsString(self->m_imageData);
for (size_t i = 3; i < data.m_dataSize; i += 4) { size_t bufsz = self->m_width * self->m_height * sizeof(uint32_t);
if (data.m_data[i] != 255) { for (size_t i = 3; i < bufsz; i += 4) {
delete[] data.m_data; if (data[i] != 255) {
return PyBool_FromLong(1); return PyBool_FromLong(1);
} }
} }
delete[] data.m_data;
return PyBool_FromLong(0); return PyBool_FromLong(0);
} }
static PyObject* pyGLTexture_get_image_data(pyGLTexture* self, void*) {
Py_XINCREF(self->m_imageData);
return Py_BuildValue("iiO", self->m_width, self->m_height, self->m_imageData);
}
static int pyGLTexture_set_image_data(pyGLTexture* self, PyObject* value, void*) {
PyObject* data;
// Requesting a Bytes object "S" instead of a buffer "y#" so we can just increment the reference
// count on a buffer that already exists, instead of doing a memcpy.
if (!PyArg_ParseTuple(value, "iiS", &self->m_width, &self->m_height, &data)) {
PyErr_SetString(PyExc_TypeError, "image_data should be a sequence of int, int, bytes");
return -1;
}
Py_XDECREF(self->m_imageData);
Py_XINCREF(data);
self->m_imageData = data;
return 0;
}
static PyObject* pyGLTexture_get_num_levels(pyGLTexture* self, void*) { static PyObject* pyGLTexture_get_num_levels(pyGLTexture* self, void*) {
return PyLong_FromLong(_get_num_levels(self)); return PyLong_FromLong(_get_num_levels(self->m_width, self->m_height));
}
static PyObject* pyGLTexture_get_size_npot(pyGLTexture* self, void*) {
return Py_BuildValue("ii", self->m_width, self->m_height);
}
static PyObject* pyGLTexture_get_size_pot(pyGLTexture* self, void*) {
size_t width = _ensure_power_of_two(self->m_width);
size_t height = _ensure_power_of_two(self->m_height);
return Py_BuildValue("ii", width, height);
} }
static PyGetSetDef pyGLTexture_GetSet[] = { static PyGetSetDef pyGLTexture_GetSet[] = {
{ _pycs("has_alpha"), (getter)pyGLTexture_get_has_alpha, NULL, NULL, NULL }, { _pycs("has_alpha"), (getter)pyGLTexture_get_has_alpha, NULL, NULL, NULL },
{ _pycs("image_data"), (getter)pyGLTexture_get_image_data, (setter)pyGLTexture_set_image_data, NULL, NULL },
{ _pycs("num_levels"), (getter)pyGLTexture_get_num_levels, NULL, NULL, NULL }, { _pycs("num_levels"), (getter)pyGLTexture_get_num_levels, NULL, NULL, NULL },
{ _pycs("size_npot"), (getter)pyGLTexture_get_size_npot, NULL, NULL, NULL },
{ _pycs("size_pot"), (getter)pyGLTexture_get_size_pot, NULL, NULL, NULL },
{ NULL, NULL, NULL, NULL, NULL } { NULL, NULL, NULL, NULL, NULL }
}; };
@ -429,6 +597,3 @@ PyObject* Init_pyGLTexture_Type() {
Py_INCREF(&pyGLTexture_Type); Py_INCREF(&pyGLTexture_Type);
return (PyObject*)&pyGLTexture_Type; return (PyObject*)&pyGLTexture_Type;
} }
};

2
korlib/texture.h

@ -21,6 +21,8 @@
extern "C" { extern "C" {
PyObject* scale_image(PyObject*, PyObject*, PyObject*);
extern PyTypeObject pyGLTexture_Type; extern PyTypeObject pyGLTexture_Type;
PyObject* Init_pyGLTexture_Type(); PyObject* Init_pyGLTexture_Type();

4
korman/exporter/convert.py

@ -350,6 +350,10 @@ class Exporter:
self.mgr.save_age(Path(self._op.filepath)) self.mgr.save_age(Path(self._op.filepath))
self.image.save() self.image.save()
@property
def envmap_method(self):
return bpy.context.scene.world.plasma_age.envmap_method
@property @property
def texcache_path(self): def texcache_path(self):
age = bpy.context.scene.world.plasma_age age = bpy.context.scene.world.plasma_age

45
korman/exporter/image.py

@ -46,6 +46,8 @@ class _EntryBits(enum.IntEnum):
source_size = 4 source_size = 4
export_size = 5 export_size = 5
last_export = 6 last_export = 6
image_count = 7
tag_string = 8
class _CachedImage: class _CachedImage:
@ -59,6 +61,8 @@ class _CachedImage:
self.compression = None self.compression = None
self.export_time = None self.export_time = None
self.modify_time = None self.modify_time = None
self.image_count = 1
self.tag = None
def __str__(self): def __str__(self):
return self.name return self.name
@ -71,10 +75,10 @@ class ImageCache:
self._read_stream = hsFileStream() self._read_stream = hsFileStream()
self._stream_handles = 0 self._stream_handles = 0
def add_texture(self, texture, num_levels, export_size, compression, data): def add_texture(self, texture, num_levels, export_size, compression, images):
image = texture.image image, tag = texture.image, texture.tag
image_name = str(texture) image_name = str(texture)
key = (image_name, compression) key = (image_name, tag, compression)
ex_method, im_method = self._exporter().texcache_method, image.plasma_image.texcache_method ex_method, im_method = self._exporter().texcache_method, image.plasma_image.texcache_method
method = set((ex_method, im_method)) method = set((ex_method, im_method))
if texture.ephemeral or "skip" in method: if texture.ephemeral or "skip" in method:
@ -89,7 +93,9 @@ class ImageCache:
image.compression = compression image.compression = compression
image.source_size = texture.image.size image.source_size = texture.image.size
image.export_size = export_size image.export_size = export_size
image.image_data = data image.image_data = images
image.image_count = len(images)
image.tag = tag
self._images[key] = image self._images[key] = image
def _compact(self): def _compact(self):
@ -111,7 +117,7 @@ class ImageCache:
self._read_stream.close() self._read_stream.close()
def get_from_texture(self, texture, compression): def get_from_texture(self, texture, compression):
bl_image = texture.image bl_image, tag = texture.image, texture.tag
# If the texture is ephemeral (eg a lightmap) or has been marked "rebuild" or "skip" # If the texture is ephemeral (eg a lightmap) or has been marked "rebuild" or "skip"
# in the UI, we don't want anything from the cache. In the first two cases, we never # in the UI, we don't want anything from the cache. In the first two cases, we never
@ -121,7 +127,7 @@ class ImageCache:
if method != {"use"} or texture.ephemeral: if method != {"use"} or texture.ephemeral:
return None return None
key = (str(texture), compression) key = (str(texture), tag, compression)
cached_image = self._images.get(key) cached_image = self._images.get(key)
if cached_image is None: if cached_image is None:
return None return None
@ -199,7 +205,9 @@ class ImageCache:
# between iterations, so we'd best bookkeep the position # between iterations, so we'd best bookkeep the position
pos = stream.pos pos = stream.pos
for i in range(image.mip_levels): def _read_image_mips():
for _ in range(image.mip_levels):
nonlocal pos
if stream.pos != pos: if stream.pos != pos:
stream.seek(pos) stream.seek(pos)
assert stream.read(4) == _MIP_MAGICK assert stream.read(4) == _MIP_MAGICK
@ -211,6 +219,12 @@ class ImageCache:
pos = stream.pos pos = stream.pos
yield data yield data
for _ in range(image.image_count):
if stream.pos != pos:
stream.seek(pos)
yield tuple(_read_image_mips())
def _read_index(self, index_pos, stream): def _read_index(self, index_pos, stream):
stream.seek(index_pos) stream.seek(index_pos)
assert stream.read(4) == _INDEX_MAGICK assert stream.read(4) == _INDEX_MAGICK
@ -250,9 +264,14 @@ class ImageCache:
image.export_size = (stream.readInt(), stream.readInt()) image.export_size = (stream.readInt(), stream.readInt())
if flags[_EntryBits.last_export]: if flags[_EntryBits.last_export]:
image.export_time = stream.readDouble() image.export_time = stream.readDouble()
if flags[_EntryBits.image_count]:
image.image_count = stream.readInt()
if flags[_EntryBits.tag_string]:
# tags should not contain user data, so we will use a latin_1 backed string
image.tag = stream.readSafeStr()
# do we need to check for duplicate images? # do we need to check for duplicate images?
self._images[(image.name, image.compression)] = image self._images[(image.name, image.tag, image.compression)] = image
@property @property
def _report(self): def _report(self):
@ -300,9 +319,10 @@ class ImageCache:
flags.write(stream) flags.write(stream)
for i in image.image_data: for i in image.image_data:
for j in i:
stream.write(_MIP_MAGICK) stream.write(_MIP_MAGICK)
stream.writeInt(len(i)) stream.writeInt(len(j))
stream.write(i) stream.write(j)
def _write_index(self, stream): def _write_index(self, stream):
flags = hsBitVector() flags = hsBitVector()
@ -327,6 +347,8 @@ class ImageCache:
flags[_EntryBits.source_size] = True flags[_EntryBits.source_size] = True
flags[_EntryBits.export_size] = True flags[_EntryBits.export_size] = True
flags[_EntryBits.last_export] = True flags[_EntryBits.last_export] = True
flags[_EntryBits.image_count] = True
flags[_EntryBits.tag_string] = image.tag is not None
stream.write(_ENTRY_MAGICK) stream.write(_ENTRY_MAGICK)
flags.write(stream) flags.write(stream)
@ -339,3 +361,6 @@ class ImageCache:
stream.writeInt(image.export_size[0]) stream.writeInt(image.export_size[0])
stream.writeInt(image.export_size[1]) stream.writeInt(image.export_size[1])
stream.writeDouble(time.time()) stream.writeDouble(time.time())
stream.writeInt(image.image_count)
if image.tag is not None:
stream.writeSafeStr(image.tag)

281
korman/exporter/material.py

@ -26,6 +26,11 @@ from . import utils
_MAX_STENCILS = 6 _MAX_STENCILS = 6
# Blender cube map mega image to libHSPlasma plCubicEnvironmap faces mapping...
# See https://blender.stackexchange.com/questions/46891/how-to-render-an-environment-to-a-cube-map-in-cycles
BLENDER_CUBE_MAP = ("leftFace", "backFace", "rightFace",
"bottomFace", "topFace", "frontFace")
class _Texture: class _Texture:
_DETAIL_BLEND = { _DETAIL_BLEND = {
TEX_DETAIL_ALPHA: "AL", TEX_DETAIL_ALPHA: "AL",
@ -40,7 +45,7 @@ class _Texture:
if texture is not None: if texture is not None:
if image is None: if image is None:
image = texture.image image = texture.image
self.calc_alpha = texture.use_calculate_alpha self.calc_alpha = getattr(texture, "use_calculate_alpha", False)
self.mipmap = texture.use_mipmap self.mipmap = texture.use_mipmap
else: else:
self.layer = kwargs.get("layer") self.layer = kwargs.get("layer")
@ -58,6 +63,7 @@ class _Texture:
self.calc_alpha = False self.calc_alpha = False
self.use_alpha = True self.use_alpha = True
self.allowed_formats = {"DDS"} self.allowed_formats = {"DDS"}
self.is_cube_map = False
else: else:
self.is_detail_map = False self.is_detail_map = False
use_alpha = kwargs.get("use_alpha") use_alpha = kwargs.get("use_alpha")
@ -70,6 +76,7 @@ class _Texture:
self.use_alpha = use_alpha self.use_alpha = use_alpha
self.allowed_formats = kwargs.get("allowed_formats", self.allowed_formats = kwargs.get("allowed_formats",
{"DDS"} if self.mipmap else {"PNG", "JPG"}) {"DDS"} if self.mipmap else {"PNG", "JPG"})
self.is_cube_map = kwargs.get("is_cube_map", False)
# Basic format sanity # Basic format sanity
if self.mipmap: if self.mipmap:
@ -84,13 +91,14 @@ class _Texture:
self.extension = kwargs.get("extension", self.auto_ext) self.extension = kwargs.get("extension", self.auto_ext)
self.ephemeral = kwargs.get("ephemeral", False) self.ephemeral = kwargs.get("ephemeral", False)
self.image = image self.image = image
self.tag = kwargs.get("tag", None)
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, _Texture): if not isinstance(other, _Texture):
return False return False
# Yeah, the string name is a unique identifier. So shoot me. # Yeah, the string name is a unique identifier. So shoot me.
if str(self) == str(other): if str(self) == str(other) and self.tag == other.tag:
self._update(other) self._update(other)
return True return True
return False return False
@ -139,13 +147,41 @@ class MaterialConverter:
"transformCtl": self._export_layer_transform_animation, "transformCtl": self._export_layer_transform_animation,
} }
def _can_export_texslot(self, slot):
if slot is None or not slot.use:
return False
texture = slot.texture
if texture is None or texture.type not in self._tex_exporters:
return False
# Per-texture type rules
if texture.type == "ENVIRONMENT_MAP":
envmap = texture.environment_map
# If this is a static, image based cube map, then we will allow it
# to be exported anyway. Note that as of the writing of this code,
# that is kind of pointless because CEMs are not yet implemented...
if envmap.source == "IMAGE_FILE":
return True
# Now for the ruelz
method, ver = self._exporter().envmap_method, self._mgr.getVer()
if method == "skip":
return False
elif method == "dcm2dem":
return True
elif method == "perengine":
return (ver >= pvMoul and envmap.mapping == "PLANE") or envmap.mapping == "CUBE"
else:
raise NotImplementedError(method)
else:
return True
def export_material(self, bo, bm): def export_material(self, bo, bm):
"""Exports a Blender Material as an hsGMaterial""" """Exports a Blender Material as an hsGMaterial"""
self._report.msg("Exporting Material '{}'", bm.name, indent=1) self._report.msg("Exporting Material '{}'", bm.name, indent=1)
hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo) hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo)
slots = [(idx, slot) for idx, slot in enumerate(bm.texture_slots) if slot is not None and slot.use \ slots = [(idx, slot) for idx, slot in enumerate(bm.texture_slots) if self._can_export_texslot(slot)]
and slot.texture is not None and slot.texture.type in self._tex_exporters]
# There is a major difference in how Blender and Plasma handle stencils. # There is a major difference in how Blender and Plasma handle stencils.
# In Blender, the stencil is on top and applies to every layer below is. In Plasma, the stencil # In Blender, the stencil is on top and applies to every layer below is. In Plasma, the stencil
@ -453,19 +489,45 @@ class MaterialConverter:
texture = slot.texture texture = slot.texture
bl_env = texture.environment_map bl_env = texture.environment_map
if bl_env.source in {"STATIC", "ANIMATED"}: if bl_env.source in {"STATIC", "ANIMATED"}:
# NOTE: It is assumed that if we arrive here, we are at lease dcm2dem on the
# environment map export method. You're welcome!
if bl_env.mapping == "PLANE" and self._mgr.getVer() >= pvMoul: if bl_env.mapping == "PLANE" and self._mgr.getVer() >= pvMoul:
pl_env = plDynamicCamMap pl_env = plDynamicCamMap
else: else:
pl_env = plDynamicEnvMap pl_env = plDynamicEnvMap
pl_env = self.export_dynamic_env(bo, layer, texture, pl_env) pl_env = self.export_dynamic_env(bo, layer, texture, pl_env)
elif bl_env.source == "IMAGE_FILE":
pl_env = self.export_cubic_env(bo, layer, texture)
else: else:
# We should really export a CubicEnvMap here, but we have a good setup for DynamicEnvMaps raise NotImplementedError(bl_env.source)
# that create themselves when the explorer links in, so really... who cares about CEMs?
self._exporter().report.warn("IMAGE EnvironmentMaps are not supported. '{}' will not be exported!".format(layer.key.name))
pl_env = None
layer.state.shadeFlags |= hsGMatState.kShadeEnvironMap layer.state.shadeFlags |= hsGMatState.kShadeEnvironMap
if pl_env is not None:
layer.texture = pl_env.key layer.texture = pl_env.key
def export_cubic_env(self, bo, layer, texture):
width, height = texture.image.size
# Sanity check: the image here should be 3x2 faces, so we should not have any
# dam remainder...
if width % 3 != 0:
raise ExportError("CubeMap '{}' width must be a multiple of 3".format(image.name))
if height % 2 != 0:
raise ExportError("CubeMap '{}' height must be a multiple of 2".format(image.name))
# According to PlasmaMAX, we don't give a rip about UVs...
layer.UVWSrc = plLayerInterface.kUVWReflect
layer.state.miscFlags |= hsGMatState.kMiscUseReflectionXform
# Well, this is kind of sad...
# Back before the texture cache existed, all the image work was offloaded
# to a big "finalize" save step to prevent races. The texture cache would
# prevent that as well, so we could theoretically slice-and-dice the single
# image here... but... meh. Offloading taim.
self.export_prepared_image(texture=texture, owner=layer, indent=3,
use_alpha=False, mipmap=True, allowed_formats={"DDS"},
is_cube_map=True, tag="cubemap")
def export_dynamic_env(self, bo, layer, texture, pl_class): def export_dynamic_env(self, bo, layer, texture, pl_class):
# To protect the user from themselves, let's check to make sure that a DEM/DCM matching this # To protect the user from themselves, let's check to make sure that a DEM/DCM matching this
# viewpoint object has not already been exported... # viewpoint object has not already been exported...
@ -662,6 +724,10 @@ class MaterialConverter:
- indent: (optional) indentation level for log messages - indent: (optional) indentation level for log messages
default: 2 default: 2
- ephemeral: (optional) never cache this image - ephemeral: (optional) never cache this image
- tag: (optional) an optional identifier hint that allows multiple images with the
same name to coexist in the cache
- is_cube_map: (optional) indicates the provided image contains six cube faces
that must be split into six separate images for Plasma
""" """
owner = kwargs.pop("owner", None) owner = kwargs.pop("owner", None)
indent = kwargs.pop("indent", 2) indent = kwargs.pop("indent", 2)
@ -690,12 +756,10 @@ class MaterialConverter:
for key, owners in self._pending.items(): for key, owners in self._pending.items():
name = str(key) name = str(key)
self._report.msg("\n[Mipmap '{}']", name) pClassName = "CubicEnvironmap" if key.is_cube_map else "Mipmap"
self._report.msg("\n[{} '{}']", pClassName, name)
image = key.image image = key.image
oWidth, oHeight = image.size
if oWidth == 0 and oHeight == 0:
raise ExportError("Image '{}' could not be loaded.".format(image.name))
# Now we try to use the pile of hints we were given to figure out what format to use # Now we try to use the pile of hints we were given to figure out what format to use
allowed_formats = key.allowed_formats allowed_formats = key.allowed_formats
@ -717,47 +781,13 @@ class MaterialConverter:
cached_image = texcache.get_from_texture(key, compression) cached_image = texcache.get_from_texture(key, compression)
if cached_image is None: if cached_image is None:
eWidth = helpers.ensure_power_of_two(oWidth) if key.is_cube_map:
eHeight = helpers.ensure_power_of_two(oHeight) numLevels, width, height, data = self._finalize_cube_map(key, image, name, compression, dxt)
if (eWidth != oWidth) or (eHeight != oHeight):
self._report.msg("Image is not a POT ({}x{}) resizing to {}x{}",
oWidth, oHeight, eWidth, eHeight, indent=1)
self._resize_image(image, eWidth, eHeight)
# Grab the image data from OpenGL and stuff it into the plBitmap
helper = GLTexture(key)
with helper as glimage:
if compression == plBitmap.kDirectXCompression:
numLevels = glimage.num_levels
self._report.msg("Generating mip levels", indent=1)
glimage.generate_mipmap()
else: else:
numLevels = 1 numLevels, width, height, data = self._finalize_single_image(key, image, name, compression, dxt)
self._report.msg("Compressing single level", indent=1) texcache.add_texture(key, numLevels, (width, height), compression, data)
# Non-DXT images are BGRA in Plasma
fmt = compression != plBitmap.kDirectXCompression
# Hold the uncompressed level data for now. We may have to make multiple copies of
# this mipmap for per-page textures :(
data = []
for i in range(numLevels):
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()
# If this is a DXT-compressed mipmap, we need to use a temporary mipmap
# to do the compression. We'll then steal the data from it.
if compression == plBitmap.kDirectXCompression:
mipmap = plMipmap(name=name, width=eWidth, height=eHeight, numLevels=numLevels,
compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt)
for i in range(numLevels):
mipmap.CompressImage(i, data[i])
data[i] = mipmap.getLevel(i)
texcache.add_texture(key, numLevels, (eWidth, eHeight), compression, data)
else: else:
eWidth, eHeight = cached_image.export_size width, height = cached_image.export_size
data = cached_image.image_data data = cached_image.image_data
numLevels = cached_image.mip_levels numLevels = cached_image.mip_levels
@ -771,28 +801,148 @@ class MaterialConverter:
self._report.msg("[{} '{}']", owner.ClassName()[2:], owner_key.name, indent=2) self._report.msg("[{} '{}']", owner.ClassName()[2:], owner_key.name, indent=2)
page = mgr.get_textures_page(owner_key) # Layer's page or Textures.prp page = mgr.get_textures_page(owner_key) # Layer's page or Textures.prp
# If we haven't created this plMipmap in the page (either layer's page or Textures.prp), # If we haven't created this texture in the page (either layer's page or Textures.prp),
# then we need to do that and stuff the level data. This is a little tedious, but we # then we need to do that and stuff the level data. This is a little tedious, but we
# need to be careful to manage our resources correctly # need to be careful to manage our resources correctly
if page not in pages: if page not in pages:
mipmap = plMipmap(name=name, width=eWidth, height=eHeight, numLevels=numLevels, mipmap = plMipmap(name=name, width=width, height=height, numLevels=numLevels,
compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt) compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt)
for i, buf in enumerate(data): if key.is_cube_map:
mipmap.setLevel(i, buf) assert len(data) == 6
mgr.AddObject(page, mipmap) texture = plCubicEnvironmap(name)
pages[page] = mipmap for face_name, face_data in zip(BLENDER_CUBE_MAP, data):
for i in range(numLevels):
mipmap.setLevel(i, face_data[i])
setattr(texture, face_name, mipmap)
else:
assert len(data) == 1
for i in range(numLevels):
mipmap.setLevel(i, data[0][i])
texture = mipmap
mgr.AddObject(page, texture)
pages[page] = texture
else: else:
mipmap = pages[page] texture = pages[page]
# The object that references this image can be either a layer (will appear
# in the 3d world) or an image library (will appear in a journal or in another
# dynamic manner in game)
if isinstance(owner, plLayerInterface): if isinstance(owner, plLayerInterface):
owner.texture = mipmap.key owner.texture = texture.key
elif isinstance(owner, plImageLibMod): elif isinstance(owner, plImageLibMod):
owner.addImage(mipmap.key) owner.addImage(texture.key)
else: else:
raise RuntimeError(owner.ClassName()) raise RuntimeError(owner.ClassName())
inc_progress() inc_progress()
def _finalize_cube_map(self, key, image, name, compression, dxt):
oWidth, oHeight = image.size
if oWidth == 0 and oHeight == 0:
raise ExportError("Image '{}' could not be loaded.".format(image.name))
# Non-DXT images are BGRA in Plasma
bgra = compression != plBitmap.kDirectXCompression
# Grab the cube map data from OpenGL and prepare to begin...
with GLTexture(key, bgra=bgra) as glimage:
cWidth, cHeight, data = glimage.image_data
# On some platforms, Blender will be "helpful" and scale the image to a POT.
# That's great, but we have 3 faces as a width, which will certainly be NPOT
# in the case of POT faces. So, we will scale the image AGAIN, if Blender did
# something funky.
if oWidth != cWidth or oHeight != cHeight:
self._report.warn("Image was resized by Blender to ({}x{})--resizing the resize to ({}x{})",
cWidth, cHeight, oWidth, oHeight, indent=1)
data = scale_image(data, cWidth, cHeight, oWidth, oHeight)
# Face dimensions
fWidth, fHeight = oWidth // 3, oHeight // 2
# Copy each of the six faces into a separate image buffer.
# NOTE: At present, I am well pleased with the speed of this functionality.
# According to my profiling, it takes roughly 0.7 seconds to process a
# cube map whose faces are 1024x1024 (3072x2048 total). Maybe a later
# commit will move this into korlib. We'll see.
face_num = len(BLENDER_CUBE_MAP)
face_images = [None] * face_num
for i in range(face_num):
col_id = i if i < 3 else i - 3
row_start = 0 if i < 3 else fHeight
row_end = fHeight if i < 3 else oHeight
face_data = bytearray(fWidth * fHeight * 4)
for row_current in range(row_start, row_end, 1):
src_start_idx = (row_current * oWidth * 4) + (col_id * fWidth * 4)
src_end_idx = src_start_idx + (fWidth * 4)
dst_start_idx = (row_current - row_start) * fWidth * 4
dst_end_idx = dst_start_idx + (fWidth * 4)
face_data[dst_start_idx:dst_end_idx] = data[src_start_idx:src_end_idx]
face_images[i] = bytes(face_data)
# Now that we have our six faces, we'll toss them into the GLTexture helper
# to generate mipmaps, if needed...
for i, face_name in enumerate(BLENDER_CUBE_MAP):
glimage = GLTexture(key)
glimage.image_data = fWidth, fHeight, face_images[i]
eWidth, eHeight = glimage.size_pot
name = face_name[:-4].upper()
if compression == plBitmap.kDirectXCompression:
numLevels = glimage.num_levels
self._report.msg("Generating mip levels for cube face '{}'", name, indent=1)
# If we're compressing this mofo, we'll need a temporary mipmap to do that here...
mipmap = plMipmap(name=name, width=eWidth, height=eHeight, numLevels=numLevels,
compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt)
else:
numLevels = 1
self._report.msg("Compressing single level for cube face '{}'", name, indent=1)
face_images[i] = [None] * numLevels
for j in range(numLevels):
level_data = glimage.get_level_data(j, key.calc_alpha, report=self._report)
if compression == plBitmap.kDirectXCompression:
mipmap.CompressImage(j, level_data)
level_data = mipmap.getLevel(j)
face_images[i][j] = level_data
return numLevels, eWidth, eHeight, face_images
def _finalize_single_image(self, key, image, name, compression, dxt):
oWidth, oHeight = image.size
if oWidth == 0 and oHeight == 0:
raise ExportError("Image '{}' could not be loaded.".format(image.name))
# Non-DXT images are BGRA in Plasma
bgra = compression != plBitmap.kDirectXCompression
# Grab the image data from OpenGL and stuff it into the plBitmap
with GLTexture(key, bgra=bgra) as glimage:
eWidth, eHeight = glimage.size_pot
if compression == plBitmap.kDirectXCompression:
numLevels = glimage.num_levels
self._report.msg("Generating mip levels", indent=1)
# If this is a DXT-compressed mipmap, we need to use a temporary mipmap
# to do the compression. We'll then steal the data from it.
mipmap = plMipmap(name=name, width=eWidth, height=eHeight, numLevels=numLevels,
compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt)
else:
numLevels = 1
self._report.msg("Compressing single level", indent=1)
# Hold the uncompressed level data for now. We may have to make multiple copies of
# this mipmap for per-page textures :(
data = [None] * numLevels
for i in range(numLevels):
level_data = glimage.get_level_data(i, key.calc_alpha, report=self._report)
if compression == plBitmap.kDirectXCompression:
mipmap.CompressImage(i, level_data)
level_data = mipmap.getLevel(i)
data[i] = level_data
return numLevels, eWidth, eHeight, [data,]
def get_materials(self, bo): def get_materials(self, bo):
return self._obj2mat.get(bo, []) return self._obj2mat.get(bo, [])
@ -849,15 +999,6 @@ class MaterialConverter:
def _report(self): def _report(self):
return self._exporter().report return self._exporter().report
def _resize_image(self, image, width, height):
image.scale(width, height)
image.update()
# If the image is already loaded into OpenGL, we need to refresh it to get the scaling.
if image.bindcode[0] != 0:
image.gl_free()
image.gl_load()
def _test_image_alpha(self, image): def _test_image_alpha(self, image):
"""Tests to see if this image has any alpha data""" """Tests to see if this image has any alpha data"""
@ -873,7 +1014,7 @@ class MaterialConverter:
else: else:
# Using bpy.types.Image.pixels is VERY VERY VERY slow... # Using bpy.types.Image.pixels is VERY VERY VERY slow...
key = _Texture(image=image) key = _Texture(image=image)
with GLTexture(key) as glimage: with GLTexture(key, fast=True) as glimage:
result = glimage.has_alpha result = glimage.has_alpha
self._alphatest[image] = result self._alphatest[image] = result

2
korman/helpers.py

@ -39,7 +39,7 @@ class TemporaryObject:
self._remove_func = remove_func self._remove_func = remove_func
def __enter__(self): def __enter__(self):
return self return self._obj
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
self._remove_func(self._obj) self._remove_func(self._obj)

221
korman/korlib/texture.py

@ -13,12 +13,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import array
import bgl import bgl
from ..helpers import ensure_power_of_two
import math import math
from PyHSPlasma import plBitmap from PyHSPlasma import plBitmap
# BGL doesn't know about this as of Blender 2.74 # BGL doesn't know about this as of Blender 2.74
bgl.GL_GENERATE_MIPMAP = 0x8191
bgl.GL_BGRA = 0x80E1 bgl.GL_BGRA = 0x80E1
# Some texture generation flags # Some texture generation flags
@ -26,35 +27,122 @@ TEX_DETAIL_ALPHA = 0
TEX_DETAIL_ADD = 1 TEX_DETAIL_ADD = 1
TEX_DETAIL_MULTIPLY = 2 TEX_DETAIL_MULTIPLY = 2
def scale_image(buf, srcW, srcH, dstW, dstH):
"""Scales an RGBA image using the algorithm from CWE's plMipmap::ScaleNicely"""
dst, dst_idx = bytearray(dstW * dstH * 4), 0
scaleX, scaleY = (srcW / dstW), (srcH / dstH)
filterW, filterH = max(scaleX, 1.0), max(scaleY, 1.0)
src_rowspan = srcW * 4
weightsY = array.array("f", [0.0] * 16)
weightsX = array.array("f", [0.0] * 16)
# I hope you're in no particular hurry...
for dstY in range(dstH):
srcY = dstY * scaleY
srcY_start = int(max(srcY - filterH, 0))
srcY_end = int(min(srcY + filterH, srcH - 1))
#weightsY = { i - srcY_start: 1.0 - abs(i - srcY) / scaleY \
# for i in range(srcY_start, srcY_end+1, 1) if i - srcY_start < 16 }
for i in range(16):
idx = i + srcY_start
if idx > srcY_end:
break
weightsY[i] = 1.0 - abs(idx - srcY) / filterH
for dstX in range(dstW):
srcX = dstX * scaleX
srcX_start = int(max(srcX - filterW, 0))
srcX_end = int(min(srcX + filterW, srcW - 1))
#weightsX = { i - srcX_start: 1.0 - abs(i - srcX) / scaleX \
# for i in range(srcX_start, srcX_end+1, 1) if i - srcX_start < 16 }
for i in range(16):
idx = i + srcX_start
if idx > srcX_end:
break
weightsX[i] = 1.0 - abs(idx - srcX) / filterW
accum_color = [0.0, 0.0, 0.0, 0.0]
weight_total = 0.0
for i in range(srcY_start, srcY_end+1, 1):
weightY_idx = i - srcY_start
weightY = weightsY[weightY_idx] if weightY_idx < 16 else 1.0 - abs(i - srcY) / filterH
weightY = 1.0 - abs(i - srcY) / filterH
src_idx = (i * src_rowspan) + (srcX_start * 4)
for j in range(srcX_start, srcX_end+1, 1):
weightX_idx = j - srcX_start
weightX = weightsX[weightX_idx] if weightX_idx < 16 else 1.0 - abs(j - srcX) / filterW
weight = weightY * weightX
if weight > 0.0:
# According to profiling, a list comprehension here doubles the execution time of this
# function. I know this function is supposed to be slow, but dayum... I've unrolled it
# to avoid all the extra allocations.
for k in range(4):
accum_color[k] = accum_color[k] + buf[src_idx+k] * weight
weight_total += weight
src_idx += 4
weight_total = max(weight_total, 0.0001)
for i in range(4):
accum_color[i] = int(accum_color[i] * (1.0 / weight_total))
dst[dst_idx:dst_idx+4] = accum_color
dst_idx += 4
return bytes(dst)
class GLTexture: class GLTexture:
def __init__(self, texkey=None): def __init__(self, texkey=None, image=None, bgra=False, fast=False):
assert texkey or image
self._texkey = texkey self._texkey = texkey
self._ownit = (self._blimg.bindcode[0] == 0) if texkey is not None:
self._blimg = texkey.image
@property if image is not None:
def _blimg(self): self._blimg = image
return self._texkey.image self._image_inverted = fast
self._bgra = bgra
def __enter__(self): def __enter__(self):
"""Sets the Blender Image as the active OpenGL texture""" """Loads the image data using OpenGL"""
if self._ownit:
# Set image active in OpenGL
ownit = self._blimg.bindcode[0] == 0
if ownit:
if self._blimg.gl_load() != 0: if self._blimg.gl_load() != 0:
raise RuntimeError("failed to load image") raise RuntimeError("failed to load image")
previous_texture = self._get_integer(bgl.GL_TEXTURE_BINDING_2D)
self._previous_texture = self._get_integer(bgl.GL_TEXTURE_BINDING_2D) changed_state = (previous_texture != self._blimg.bindcode[0])
self._changed_state = (self._previous_texture != self._blimg.bindcode[0]) if changed_state:
if self._changed_state:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._blimg.bindcode[0]) bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._blimg.bindcode[0])
# Grab the image data
self._width = self._get_tex_param(bgl.GL_TEXTURE_WIDTH, 0)
self._height = self._get_tex_param(bgl.GL_TEXTURE_HEIGHT, 0)
size = self._width * self._height * 4
buf = bgl.Buffer(bgl.GL_BYTE, size)
fmt = bgl.GL_BGRA if self._bgra else bgl.GL_RGBA
bgl.glGetTexImage(bgl.GL_TEXTURE_2D, 0, fmt, bgl.GL_UNSIGNED_BYTE, buf)
# OpenGL returns the images upside down, so we're going to rotate it in memory.
# ... But only if requested... :)
if self._image_inverted:
self._image_data = bytes(buf)
else:
self._image_data = self._invert_image(self._width, self._height, buf)
# Restore previous OpenGL state
if changed_state:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, previous_texture)
if ownit:
self._blimg.gl_free()
return self return self
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
mipmap_state = getattr(self, "_mipmap_state", None) del self._image_data
if mipmap_state is not None:
bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_GENERATE_MIPMAP, mipmap_state)
if self._changed_state:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._previous_texture)
if self._ownit:
self._blimg.gl_free()
@property @property
def _detail_falloff(self): def _detail_falloff(self):
@ -64,57 +152,55 @@ class GLTexture:
self._texkey.detail_opacity_start / 100.0, self._texkey.detail_opacity_start / 100.0,
self._texkey.detail_opacity_stop / 100.0) self._texkey.detail_opacity_stop / 100.0)
def generate_mipmap(self): def get_level_data(self, level=0, calc_alpha=False, report=None, indent=2, fast=False):
"""Generates all mip levels for this texture"""
self._mipmap_state = self._get_tex_param(bgl.GL_GENERATE_MIPMAP)
# Note that this is a very old feature from OpenGL 1.x -- it's new enough that Windows (and
# Blender apparently) don't support it natively and yet old enough that it was thrown away
# in OpenGL 3.0. The new way is glGenerateMipmap, but Blender likes oldgl, so we don't have that
# function available to us in BGL. I don't want to deal with loading the GL dll in ctypes on
# many platforms right now (or context headaches). If someone wants to fix this, be my guest!
# 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, report=None, fast=False):
"""Gets the uncompressed pixel data for a requested mip level, optionally calculating the alpha """Gets the uncompressed pixel data for a requested mip level, optionally calculating the alpha
channel from the image color data channel from the image color data
""" """
width = self._get_tex_param(bgl.GL_TEXTURE_WIDTH, level)
height = self._get_tex_param(bgl.GL_TEXTURE_HEIGHT, level) # Previously, we would leave the texture bound in OpenGL and use it to do the mipmapping, using
# old, deprecated OpenGL features. With the introduction of plCubicEnvironmap support to Korman,
# we wind up needing to get an NPOT image from OpenGL. Unfortunately, Blender will sometimes scale
# images to be POT _before_ loading them into OpenGL. Therefore, we now use OpenGL to grab the first
# level, then scale down to the new level from there.
oWidth, oHeight = self.size_npot
eWidth = ensure_power_of_two(oWidth) >> level
eHeight = ensure_power_of_two(oHeight) >> level
if report is not None: if report is not None:
report.msg("Level #{}: {}x{}", level, width, height, indent=2) report.msg("Level #{}: {}x{}", level, eWidth, eHeight, indent=indent)
# Grab the image data # Scale, if needed...
size = width * height * 4 if oWidth != eWidth or oHeight != eHeight:
buf = bgl.Buffer(bgl.GL_BYTE, size) buf = _scale_image(self._image_data, oWidth, oHeight, eWidth, eHeight)
fmt = bgl.GL_BGRA if bgra else bgl.GL_RGBA else:
bgl.glGetTexImage(bgl.GL_TEXTURE_2D, level, fmt, bgl.GL_UNSIGNED_BYTE, buf); buf = self._image_data
# Some operations, like alpha testing, don't care about the fact that OpenGL flips
# the images in memory. Give an opportunity to bail here...
if fast: if fast:
return bytes(buf) return self._image_data
else:
buf = bytearray(self._image_data)
# OpenGL returns the images upside down, so we're going to rotate it in memory.
finalBuf = bytearray(size) if self._image_inverted:
row_stride = width * 4 buf = self._invert_image(eWidth, eHeight, buf)
for i in range(height):
src, dst = i * row_stride, (height - (i+1)) * row_stride
finalBuf[dst:dst+row_stride] = buf[src:src+row_stride]
# If this is a detail map, then we need to bake that per-level here. # If this is a detail map, then we need to bake that per-level here.
if self._texkey.is_detail_map: if self._texkey is not None and self._texkey.is_detail_map:
detail_blend = self._texkey.detail_blend detail_blend = self._texkey.detail_blend
if detail_blend == TEX_DETAIL_ALPHA: if detail_blend == TEX_DETAIL_ALPHA:
self._make_detail_map_alpha(finalBuf, level) self._make_detail_map_alpha(buf, level)
elif detail_blend == TEX_DETAIL_ADD: elif detail_blend == TEX_DETAIL_ADD:
self._make_detail_map_alpha(finalBuf, level) self._make_detail_map_alpha(buf, level)
elif detail_blend == TEX_DETAIL_MULTIPLY: elif detail_blend == TEX_DETAIL_MULTIPLY:
self._make_detail_map_mult(finalBuf, level) self._make_detail_map_mult(buf, level)
# Do we need to calculate the alpha component? # Do we need to calculate the alpha component?
if calc_alpha: if calc_alpha:
for i in range(0, size, 4): for i in range(0, size, 4):
finalBuf[i+3] = int(sum(finalBuf[i:i+3]) / 3) buf[i+3] = int(sum(buf[i:i+3]) / 3)
return bytes(finalBuf) return bytes(buf)
def _get_detail_alpha(self, level, dropoff_start, dropoff_stop, detail_max, detail_min): def _get_detail_alpha(self, level, dropoff_start, dropoff_stop, detail_max, detail_min):
alpha = (level - dropoff_start) * (detail_min - detail_max) / (dropoff_stop - dropoff_start) + detail_max alpha = (level - dropoff_start) * (detail_min - detail_max) / (dropoff_stop - dropoff_start) + detail_max
@ -138,12 +224,27 @@ class GLTexture:
@property @property
def has_alpha(self): def has_alpha(self):
data = self.get_level_data(report=None, fast=True) data = self._image_data
for i in range(3, len(data), 4): for i in range(3, len(data), 4):
if data[i] != 255: if data[i] != 255:
return True return True
return False return False
def _get_image_data(self):
return (self._width, self._height, self._image_data)
def _set_image_data(self, value):
self._width, self._height, self._image_data = value
image_data = property(_get_image_data, _set_image_data)
def _invert_image(self, width, height, buf):
size = width * height * 4
finalBuf = bytearray(size)
row_stride = width * 4
for i in range(height):
src, dst = i * row_stride, (height - (i+1)) * row_stride
finalBuf[dst:dst+row_stride] = buf[src:src+row_stride]
return bytes(finalBuf)
def _make_detail_map_add(self, data, level): def _make_detail_map_add(self, data, level):
dropoff_start, dropoff_stop, detail_max, detail_min = self._detail_falloff dropoff_start, dropoff_stop, detail_max, detail_min = self._detail_falloff
alpha = self._get_detail_alpha(level, dropoff_start, dropoff_stop, detail_max, detail_min) alpha = self._get_detail_alpha(level, dropoff_start, dropoff_stop, detail_max, detail_min)
@ -167,7 +268,7 @@ class GLTexture:
@property @property
def num_levels(self): def num_levels(self):
numLevels = math.floor(math.log(max(self._blimg.size), 2)) + 1 numLevels = math.floor(math.log(max(self.size_npot), 2)) + 1
# Major Workaround Ahoy # Major Workaround Ahoy
# There is a bug in Cyan's level size algorithm that causes it to not allocate enough memory # There is a bug in Cyan's level size algorithm that causes it to not allocate enough memory
@ -181,3 +282,11 @@ class GLTexture:
# texture in a single pixel?" # texture in a single pixel?"
# :) # :)
return max(numLevels - 2, 2) return max(numLevels - 2, 2)
@property
def size_npot(self):
return self._width, self._height
@property
def size_pot(self):
return ensure_power_of_two(self._width), ensure_power_of_two(self._height)

1
korman/operators/__init__.py

@ -14,6 +14,7 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
from . import op_export as exporter from . import op_export as exporter
from . import op_image as image
from . import op_lightmap as lightmap from . import op_lightmap as lightmap
from . import op_mesh as mesh from . import op_mesh as mesh
from . import op_modifier as modifier from . import op_modifier as modifier

7
korman/operators/op_export.py

@ -64,6 +64,13 @@ class ExportOperator(bpy.types.Operator):
("force_lightmap", "Force Lightmap Bake", "All static lighting is baked as lightmaps (slower export)")], ("force_lightmap", "Force Lightmap Bake", "All static lighting is baked as lightmaps (slower export)")],
"default": "bake"}), "default": "bake"}),
"envmap_method": (EnumProperty, {"name": "Environment Maps",
"description": "Environment Map Settings",
"items": [("skip", "Don't Export EnvMaps", "Environment Maps are not exported"),
("dcm2dem", "Downgrade Planar EnvMaps", "When the engine doesn't support them, Planar Environment Maps are downgraded to Cube Maps"),
("perengine", "Export Supported EnvMaps", "Only environment maps supported by the selected game engine are exported")],
"default": "dcm2dem"}),
"export_active": (BoolProperty, {"name": "INTERNAL: Export currently running", "export_active": (BoolProperty, {"name": "INTERNAL: Export currently running",
"default": False, "default": False,
"options": {"SKIP_SAVE"}}), "options": {"SKIP_SAVE"}}),

237
korman/operators/op_image.py

@ -0,0 +1,237 @@
# 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
from bpy.props import *
from pathlib import Path
from ..helpers import TemporaryObject, ensure_power_of_two
from ..korlib import ConsoleToggler, GLTexture, scale_image
from ..exporter.explosions import *
from ..exporter.logger import ExportProgressLogger
from ..exporter.material import BLENDER_CUBE_MAP
# These are some filename suffixes that we will check to match for the cubemap faces
_CUBE_FACES = {
"leftFace": "LF",
"backFace": "BK",
"rightFace": "RT",
"bottomFace": "DN",
"topFace": "UP",
"frontFace": "FR",
}
class ImageOperator:
@classmethod
def poll(cls, context):
return context.scene.render.engine == "PLASMA_GAME"
class PlasmaBuildCubeMapOperator(ImageOperator, bpy.types.Operator):
bl_idname = "image.plasma_build_cube_map"
bl_label = "Build Cubemap"
bl_description = "Builds a Blender cubemap from six images"
overwrite_existing = BoolProperty(name="Check Existing",
description="Checks for an existing image and overwrites it",
default=True,
options=set())
filepath = StringProperty(subtype="FILE_PATH")
require_cube = BoolProperty(name="Require Square Faces",
description="Resize cubemap faces to be square if they are not",
default=True,
options=set())
texture_name = StringProperty(name="Texture",
description="Environment Map Texture to stuff this into",
default="",
options={"HIDDEN"})
def __init__(self):
self._report = ExportProgressLogger()
self._report.progress_add_step("Finding Face Images")
self._report.progress_add_step("Loading Face Images")
self._report.progress_add_step("Scaling Face Images")
self._report.progress_add_step("Generating Cube Map")
def execute(self, context):
with ConsoleToggler(True) as _:
try:
self._execute()
except ExportError as error:
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
else:
return {"FINISHED"}
def _execute(self):
self._report.progress_start("BUILDING CUBE MAP")
if not Path(self.filepath).is_file():
raise ExportError("No cube image found at '{}'".format(self.filepath))
# Figure out the paths for the six cube faces. We will use the original file
# only if a face is missing...
face_image_paths = self._find_cube_files(self.filepath)
# If no images were loaded, that means we will want to generate a cube map
# with the single face provided by the image in filepath. Otherwise, we'll
# use the found faces (and the provided path if any are missing...)
face_data = list(self._load_all_image_data(face_image_paths, self.filepath))
face_widths, face_heights, face_data = zip(*face_data)
# All widths and heights must be the same... so, if needed, scale the stupid images.
width, height, face_data = self._scale_images(face_widths, face_heights, face_data)
# Now generate the stoopid cube map
image_name = Path(self.filepath).name
idx = image_name.rfind('_')
if idx != -1:
suffix = image_name[idx+1:idx+3]
if suffix in _CUBE_FACES.values():
image_name = image_name[:idx] + image_name[idx+3:]
cubemap_image = self._generate_cube_map(image_name, width, height, face_data)
# If a texture was provided, we can assign this generated cube map to it...
if self.texture_name:
texture = bpy.data.textures[self.texture_name]
texture.environment_map.source = "IMAGE_FILE"
texture.image = cubemap_image
self._report.progress_end()
return {"FINISHED"}
def _find_cube_files(self, filepath):
self._report.progress_advance()
self._report.progress_range = len(BLENDER_CUBE_MAP)
self._report.msg("Searching for cubemap faces...")
idx = filepath.rfind('_')
if idx != -1:
files = []
for key in BLENDER_CUBE_MAP:
suffix = _CUBE_FACES[key]
face_path = filepath[:idx+1] + suffix + filepath[idx+3:]
face_name = key[:-4].upper()
if Path(face_path).is_file():
self._report.msg("Found face '{}': {}", face_name, face_path, indent=1)
files.append(face_path)
else:
self._report.warn("Using default face data for face '{}'", face_name, indent=1)
files.append(None)
self._report.progress_increment()
return tuple(files)
def _generate_cube_map(self, req_name, face_width, face_height, face_data):
self._report.progress_advance()
self._report.msg("Generating cubemap image...")
# If a texture was provided, we should check to see if we have an image we can replace...
image = bpy.data.textures[self.texture_name].image if self.texture_name else None
# Init our image
image_width = face_width * 3
image_height = face_height * 2
if image is not None and self.overwrite_existing:
image.source = "GENERATED"
image.generated_width = image_width
image.generated_height = image_height
else:
image = bpy.data.images.new(req_name, image_width, image_height, True)
image_datasz = image_width * image_height * 4
image_data = bytearray(image_datasz)
face_num = len(BLENDER_CUBE_MAP)
# This is the inverse of the operation found in MaterialConverter._finalize_cube_map
for i in range(face_num):
col_id = i if i < 3 else i - 3
row_start = 0 if i < 3 else face_height
row_end = face_height if i < 3 else image_height
# TIL: Blender's coordinate system has its origin in the lower left, while Plasma's
# is in the upper right. We could do some fancy flipping stuff, but there are already
# mitigations in code for that. So, we disabled the GLTexture's flipping helper and
# will just swap the locations of the images in the list. le wout.
j = i + 3 if i < 3 else i - 3
for row_current in range(row_start, row_end, 1):
src_start_idx = (row_current - row_start) * face_width * 4
src_end_idx = src_start_idx + (face_width * 4)
dst_start_idx = (row_current * image_width * 4) + (col_id * face_width * 4)
dst_end_idx = dst_start_idx + (face_width * 4)
image_data[dst_start_idx:dst_end_idx] = face_data[j][src_start_idx:src_end_idx]
# FFFUUUUU... Blender wants a list of floats
pixels = [None] * image_datasz
for i in range(image_datasz):
pixels[i] = image_data[i] / 255
# Obligatory remark: "Blender sucks"
image.pixels = pixels
image.update()
image.pack(True)
return image
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _load_all_image_data(self, face_image_paths, default_image_path):
self._report.progress_advance()
self._report.progress_range = len(BLENDER_CUBE_MAP)
self._report.msg("Loading cubemap faces...")
default_data = None
for image_path in face_image_paths:
if image_path is None:
if default_data is None:
default_data = self._load_single_image_data(default_image_path)
yield default_data
else:
yield self._load_single_image_data(image_path)
self._report.progress_increment()
def _load_single_image_data(self, filepath):
images = bpy.data.images
with TemporaryObject(images.load(filepath), images.remove) as blimage:
with GLTexture(image=blimage, fast=True) as glimage:
return glimage.image_data
def _scale_images(self, face_widths, face_heights, face_data):
self._report.progress_advance()
self._report.progress_range = len(BLENDER_CUBE_MAP)
self._report.msg("Checking cubemap face dimensions...")
# Take the smallest face width and get its POT variety (so we don't rescale on export)
min_width = ensure_power_of_two(min(face_widths))
min_height = ensure_power_of_two(min(face_heights))
# They're called CUBEmaps, dingus...
if self.require_cube:
dimension = min(min_width, min_height)
min_width, min_height = dimension, dimension
# Insert grumbling here about tuples being immutable...
result_data = list(face_data)
for i in range(len(BLENDER_CUBE_MAP)):
face_width, face_height = face_widths[i], face_heights[i]
if face_width != min_width or face_height != min_height:
face_name = BLENDER_CUBE_MAP[i][:-4].upper()
self._report.msg("Resizing face '{}' from {}x{} to {}x{}", face_name,
face_width, face_height, min_width, min_height,
indent=1)
result_data[i] = scale_image(face_data[i], face_width, face_height,
min_width, min_height)
self._report.progress_increment()
return min_width, min_height, tuple(result_data)

19
korman/operators/op_toolbox.py

@ -102,6 +102,25 @@ class PlasmaToggleAllPlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class PlasmaToggleEnvironmentMapsOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "texture.plasma_toggle_environment_maps"
bl_label = "Toggle Environment Maps"
bl_description = "Changes the state of all Environment Maps"
enable = BoolProperty(name="Enable", description="Enable Environment Maps")
def execute(self, context):
enable = self.enable
for material in bpy.data.materials:
for slot in material.texture_slots:
if slot is None:
continue
if slot.texture.type == "ENVIRONMENT_MAP":
slot.use = enable
return {"FINISHED"}
class PlasmaTogglePlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator): class PlasmaTogglePlasmaObjectsOperator(ToolboxOperator, bpy.types.Operator):
bl_idname = "object.plasma_toggle_selected_objects" bl_idname = "object.plasma_toggle_selected_objects"
bl_label = "Toggle Plasma Objects" bl_label = "Toggle Plasma Objects"

9
korman/ui/ui_texture.py

@ -37,9 +37,11 @@ class PlasmaEnvMapPanel(TextureButtonsPanel, bpy.types.Panel):
return False return False
def draw(self, context): def draw(self, context):
layer_props = context.texture.plasma_layer texture = context.texture
layer_props, envmap = texture.plasma_layer, texture.environment_map
layout = self.layout layout = self.layout
if envmap.source in {"ANIMATED", "STATIC"}:
layout.prop(layer_props, "envmap_color") layout.prop(layer_props, "envmap_color")
layout.separator() layout.separator()
@ -49,6 +51,11 @@ class PlasmaEnvMapPanel(TextureButtonsPanel, bpy.types.Panel):
rgns = layer_props.vis_regions rgns = layer_props.vis_regions
if layer_props.vis_regions: if layer_props.vis_regions:
layout.prop(rgns[layer_props.active_region_index], "control_region") layout.prop(rgns[layer_props.active_region_index], "control_region")
elif envmap.source == "IMAGE_FILE":
op = layout.operator("image.plasma_build_cube_map",
text="Build Cubemap from Cube Faces",
icon="MATCUBE")
op.texture_name = context.texture.name
class PlasmaLayerPanel(TextureButtonsPanel, bpy.types.Panel): class PlasmaLayerPanel(TextureButtonsPanel, bpy.types.Panel):

6
korman/ui/ui_toolbox.py

@ -41,7 +41,11 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel):
disable_all = col.operator("object.plasma_toggle_all_objects", icon="OBJECT_DATA", text="Disable All") disable_all = col.operator("object.plasma_toggle_all_objects", icon="OBJECT_DATA", text="Disable All")
disable_all.enable = False disable_all.enable = False
col.label("Textures:")
col.operator("texture.plasma_enable_all_textures", icon="TEXTURE", text="Enable All")
col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB", text="Enable All EnvMaps").enable = True
col.operator("texture.plasma_toggle_environment_maps", icon="IMAGE_RGB_ALPHA", text="Disable All EnvMaps").enable = False
col.label("Convert:") col.label("Convert:")
col.operator("object.plasma_convert_plasma_objects", icon="OBJECT_DATA", text="Plasma Objects") col.operator("object.plasma_convert_plasma_objects", icon="OBJECT_DATA", text="Plasma Objects")
col.operator("texture.plasma_enable_all_textures", icon="TEXTURE")
col.operator("texture.plasma_convert_layer_opacities", icon="IMAGE_RGB_ALPHA", text="Layer Opacities") col.operator("texture.plasma_convert_layer_opacities", icon="IMAGE_RGB_ALPHA", text="Layer Opacities")

1
korman/ui/ui_world.py

@ -143,6 +143,7 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
col.prop(age, "use_texture_page") col.prop(age, "use_texture_page")
layout.separator() layout.separator()
layout.prop(age, "envmap_method")
layout.prop(age, "lighting_method") layout.prop(age, "lighting_method")
layout.prop(age, "texcache_method") layout.prop(age, "texcache_method")

Loading…
Cancel
Save