diff --git a/korlib/korlib.h b/korlib/korlib.h index b59e4a2..99a8ba8 100644 --- a/korlib/korlib.h +++ b/korlib/korlib.h @@ -23,6 +23,7 @@ #include #define _pycs(x) const_cast(x) +#define arrsize(a) (sizeof(a) / sizeof((a)[0])) class PyObjectRef { PyObject* m_object; diff --git a/korlib/texture.cpp b/korlib/texture.cpp index 049c94e..ae26746 100644 --- a/korlib/texture.cpp +++ b/korlib/texture.cpp @@ -26,12 +26,40 @@ #include #include -#ifndef GL_GENERATE_MIPMAP -# define GL_GENERATE_MIPMAP 0x8191 -#endif // GL_GENERATE_MIPMAP - #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 +static T _ensure_power_of_two(T value) { + return static_cast(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) { PyObjectRef pyfloat = PyObject_GetAttrString(source, attr); if (pyfloat) { @@ -41,7 +69,93 @@ static inline bool _get_float(PyObject* source, const char* attr, float& result) 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. + // " 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(srcW) / static_cast(dstW); + float scaleY = static_cast(srcH) / static_cast(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(srcY - filterH), + static_cast(0)); + ssize_t srcY_end = std::min(static_cast(srcY + filterH), + static_cast(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(srcX - filterW), + static_cast(0)); + ssize_t srcX_end = std::min(static_cast(srcX + filterW), + static_cast(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(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(accum_color[k] * 255.f); + dstIdx += sizeof(uint32_t); + } + } +} enum { TEX_DETAIL_ALPHA = 0, @@ -53,10 +167,11 @@ typedef struct { PyObject_HEAD PyObject* m_blenderImage; PyObject* m_textureKey; - bool m_ownIt; - GLint m_prevImage; - bool m_changedState; - GLint m_mipmapState; + PyObject* m_imageData; + GLint m_width; + GLint m_height; + bool m_bgra; + bool m_imageInverted; } pyGLTexture; typedef struct { @@ -66,8 +181,9 @@ typedef struct { } pyMipmap; static void pyGLTexture_dealloc(pyGLTexture* self) { - Py_XDECREF(self->m_textureKey); - Py_XDECREF(self->m_blenderImage); + Py_CLEAR(self->m_textureKey); + Py_CLEAR(self->m_blenderImage); + Py_CLEAR(self->m_imageData); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -75,15 +191,18 @@ static PyObject* pyGLTexture_new(PyTypeObject* type, PyObject* args, PyObject* k pyGLTexture* self = (pyGLTexture*)type->tp_alloc(type, 0); self->m_blenderImage = NULL; self->m_textureKey = NULL; - self->m_ownIt = false; - self->m_prevImage = 0; - self->m_changedState = false; - self->m_mipmapState = 0; + self->m_imageData = NULL; + self->m_width = 0; + self->m_height = 0; + self->m_bgra = false; + self->m_imageInverted = false; return (PyObject*)self; } static int pyGLTexture___init__(pyGLTexture* self, PyObject* args, PyObject* kwds) { - if (!PyArg_ParseTuple(args, "O", &self->m_textureKey)) { + static char* kwlist[] = { _pycs("texkey"), _pycs("bgra"), _pycs("fast"), NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|bb", kwlist, &self->m_textureKey, + &self->m_bgra, &self->m_imageInverted)) { PyErr_SetString(PyExc_TypeError, "expected a korman.exporter.material._Texture"); return -1; } @@ -115,12 +234,13 @@ static PyObject* pyGLTexture__enter__(pyGLTexture* self) { return NULL; } - glGetIntegerv(GL_TEXTURE_BINDING_2D, &self->m_prevImage); + GLint prevImage; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevImage); GLuint image_bindcode = PyLong_AsUnsignedLong(bindcode); - self->m_ownIt = image_bindcode == 0; + bool ownit = image_bindcode == 0; // Load image into GL - if (self->m_ownIt) { + if (ownit) { PyObjectRef new_bind = PyObject_CallMethod(self->m_blenderImage, "gl_load", NULL); if (!PyLong_Check(new_bind)) { PyErr_SetString(PyExc_TypeError, "gl_load() did not return a long"); @@ -144,64 +264,39 @@ static PyObject* pyGLTexture__enter__(pyGLTexture* self) { } // Set image as current in GL - if (self->m_prevImage != image_bindcode) { - self->m_changedState = true; + bool changedState = prevImage != image_bindcode; + if (changedState) glBindTexture(GL_TEXTURE_2D, image_bindcode); - } - // Misc GL state - glGetTexParameteriv(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, &self->m_mipmapState); + // Now we can load the image data... + 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(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(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); return (PyObject*)self; } static PyObject* pyGLTexture__exit__(pyGLTexture* self, PyObject*) { - // We don't care about the args here - 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_CLEAR(self->m_imageData); 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. - // " 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) { float dropoff_start, dropoff_stop, detail_max, detail_min; if (!_get_float(self->m_textureKey, "detail_fade_start", dropoff_start)) @@ -214,9 +309,9 @@ static int _generate_detail_alpha(pyGLTexture* self, GLint level, float* result) return -1; 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 *= _get_num_levels(self); + dropoff_stop *= _get_num_levels(self->m_width, self->m_height); detail_max /= 100.f; detail_min /= 100.f; @@ -265,102 +360,110 @@ static int _generate_detail_map(pyGLTexture* self, uint8_t* buf, size_t bufsz, G return 0; } -static _LevelData _get_level_data(pyGLTexture* self, GLint level, bool bgra, PyObject* report) { - GLint width, height; - glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width); - glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height); - GLenum fmt = bgra ? GL_BGRA_EXT : GL_RGBA; - - // Print out the debug message - if (report && report != Py_None) { - PyObjectRef msg_func = PyObject_GetAttrString(report, "msg"); - PyObjectRef args = Py_BuildValue("siii", "Level #{}: {}x{}", level, width, height); - PyObjectRef kwargs = Py_BuildValue("{s:i}", "indent", 2); - PyObjectRef result = PyObject_Call(msg_func, args, kwargs); - } - - size_t bufsz; - bufsz = (width * height * 4); - uint8_t* buf = new uint8_t[bufsz]; - glGetTexImage(GL_TEXTURE_2D, level, fmt, GL_UNSIGNED_BYTE, reinterpret_cast(buf)); - return _LevelData(width, height, buf, bufsz); -} - static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, PyObject* kwargs) { - static char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("bgra"), - _pycs("report"), _pycs("fast"), NULL }; + static char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("report"), + _pycs("indent"), _pycs("fast"), NULL }; GLint level = 0; bool calc_alpha = false; - bool bgra = false; PyObject* report = nullptr; + int indent = 2; 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"); + 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; } - _LevelData data = _get_level_data(self, level, bgra, report); - if (fast) - return PyBytes_FromStringAndSize((char*)data.m_data, data.m_dataSize); + // 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); - // 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; + // Print out the debug message + if (report && report != Py_None) { + PyObjectRef msg_func = PyObject_GetAttrString(report, "msg"); + PyObjectRef args = Py_BuildValue("siii", "Level #{}: {}x{}", level, eWidth, eHeight); + PyObjectRef kwargs = Py_BuildValue("{s:i}", "indent", indent); + PyObjectRef result = PyObject_Call(msg_func, args, kwargs); + } + + PyObject* data; + if (is_og) { + Py_INCREF(self->m_imageData); + data = self->m_imageData; + } else { + data = PyBytes_FromStringAndSize(NULL, bufsz); + uint8_t* dstBuf = reinterpret_cast(PyBytes_AsString(data)); // AS_STRING :( + uint8_t* srcBuf = reinterpret_cast(PyBytes_AsString(self->m_imageData)); + _scale_image(srcBuf, self->m_width, self->m_height, dstBuf, eWidth, eHeight); + } + + // Make sure the level data is not flipped upside down... + if (self->m_imageInverted && !fast) { + _ensure_copy_bytes(self->m_blenderImage, data); + _flip_image(eWidth, bufsz, reinterpret_cast(PyBytes_AS_STRING(data))); + } // Detail blend PyObjectRef is_detail_map = PyObject_GetAttrString(self->m_textureKey, "is_detail_map"); if (PyLong_AsLong(is_detail_map) != 0) { - if (_generate_detail_map(self, data.m_data, data.m_dataSize, level) != 0) { - delete[] data.m_data; + _ensure_copy_bytes(self->m_imageData, data); + uint8_t* buf = reinterpret_cast(PyBytes_AS_STRING(data)); + if (_generate_detail_map(self, buf, bufsz, level) != 0) { PyErr_SetString(PyExc_RuntimeError, "error while baking detail map"); + Py_DECREF(data); return NULL; } } if (calc_alpha) { - for (size_t i = 0; i < data.m_dataSize; i += 4) - data.m_data[i + 3] = (data.m_data[i + 0] + data.m_data[i + 1] + data.m_data[i + 2]) / 3; + _ensure_copy_bytes(self->m_imageData, data); + 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[] = { { _pycs("__enter__"), (PyCFunction)pyGLTexture__enter__, METH_NOARGS, 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 }, { NULL, NULL, 0, NULL } }; static PyObject* pyGLTexture_get_has_alpha(pyGLTexture* self, void*) { - _LevelData data = _get_level_data(self, 0, false, nullptr); - for (size_t i = 3; i < data.m_dataSize; i += 4) { - if (data.m_data[i] != 255) { - delete[] data.m_data; + char* data = PyBytes_AsString(self->m_imageData); + size_t bufsz = self->m_width * self->m_height * sizeof(uint32_t); + for (size_t i = 3; i < bufsz; i += 4) { + if (data[i] != 255) { return PyBool_FromLong(1); } } - delete[] data.m_data; return PyBool_FromLong(0); } 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[] = { { _pycs("has_alpha"), (getter)pyGLTexture_get_has_alpha, 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 } }; @@ -429,6 +532,3 @@ PyObject* Init_pyGLTexture_Type() { Py_INCREF(&pyGLTexture_Type); return (PyObject*)&pyGLTexture_Type; } - -}; - diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 1654802..0258eff 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -726,9 +726,6 @@ class MaterialConverter: self._report.msg("\n[Mipmap '{}']", name) 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 allowed_formats = key.allowed_formats @@ -750,48 +747,11 @@ class MaterialConverter: cached_image = texcache.get_from_texture(key, compression) if cached_image is None: - eWidth = helpers.ensure_power_of_two(oWidth) - eHeight = helpers.ensure_power_of_two(oHeight) - 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: - numLevels = 1 - self._report.msg("Compressing single level", indent=1) - - # 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,]) + numLevels, width, height, data = self._finalize_single_image(key, image, name, compression, dxt) + texcache.add_texture(key, numLevels, (width, height), compression, data) else: - eWidth, eHeight = cached_image.export_size - data = cached_image.image_data[0] + width, height = cached_image.export_size + data = cached_image.image_data numLevels = cached_image.mip_levels # Now we poke our new bitmap into the pending layers. Note that we have to do some funny @@ -804,19 +764,23 @@ class MaterialConverter: self._report.msg("[{} '{}']", owner.ClassName()[2:], owner_key.name, indent=2) 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 # need to be careful to manage our resources correctly if page not in pages: - mipmap = plMipmap(name=name, width=eWidth, height=eHeight, numLevels=numLevels, - compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt) - for i, buf in enumerate(data): - mipmap.setLevel(i, buf) + mipmap = plMipmap(name=name, width=width, height=height, + numLevels=numLevels, compType=compression, + format=plBitmap.kRGB8888, dxtLevel=dxt) + for i in range(numLevels): + mipmap.setLevel(i, data[0][i]) mgr.AddObject(page, mipmap) pages[page] = mipmap else: mipmap = 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): owner.texture = mipmap.key elif isinstance(owner, plImageLibMod): @@ -826,6 +790,40 @@ class MaterialConverter: inc_progress() + 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) + 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 = [] + for i in range(numLevels): + data.append(glimage.get_level_data(i, key.calc_alpha, report=self._report)) + + # 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) + return numLevels, eWidth, eHeight, [data,] + def get_materials(self, bo): return self._obj2mat.get(bo, []) @@ -882,15 +880,6 @@ class MaterialConverter: def _report(self): 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): """Tests to see if this image has any alpha data""" @@ -906,7 +895,7 @@ class MaterialConverter: else: # Using bpy.types.Image.pixels is VERY VERY VERY slow... key = _Texture(image=image) - with GLTexture(key) as glimage: + with GLTexture(key, fast=True) as glimage: result = glimage.has_alpha self._alphatest[image] = result diff --git a/korman/korlib/texture.py b/korman/korlib/texture.py index 10d9388..766b68b 100644 --- a/korman/korlib/texture.py +++ b/korman/korlib/texture.py @@ -13,12 +13,13 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +import array import bgl +from ..helpers import ensure_power_of_two import math from PyHSPlasma import plBitmap # BGL doesn't know about this as of Blender 2.74 -bgl.GL_GENERATE_MIPMAP = 0x8191 bgl.GL_BGRA = 0x80E1 # Some texture generation flags @@ -26,35 +27,121 @@ TEX_DETAIL_ALPHA = 0 TEX_DETAIL_ADD = 1 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: - def __init__(self, texkey=None): + def __init__(self, texkey=None, bgra=False, fast=False): self._texkey = texkey - self._ownit = (self._blimg.bindcode[0] == 0) + self._image_inverted = fast + self._bgra = bgra @property def _blimg(self): return self._texkey.image def __enter__(self): - """Sets the Blender Image as the active OpenGL texture""" - if self._ownit: + """Loads the image data using OpenGL""" + + # Set image active in OpenGL + ownit = self._blimg.bindcode[0] == 0 + if ownit: if self._blimg.gl_load() != 0: raise RuntimeError("failed to load image") - - self._previous_texture = self._get_integer(bgl.GL_TEXTURE_BINDING_2D) - self._changed_state = (self._previous_texture != self._blimg.bindcode[0]) - if self._changed_state: + previous_texture = self._get_integer(bgl.GL_TEXTURE_BINDING_2D) + changed_state = (previous_texture != self._blimg.bindcode[0]) + if changed_state: 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 def __exit__(self, type, value, traceback): - mipmap_state = getattr(self, "_mipmap_state", None) - 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() + del self._image_data @property def _detail_falloff(self): @@ -64,57 +151,55 @@ class GLTexture: self._texkey.detail_opacity_start / 100.0, self._texkey.detail_opacity_stop / 100.0) - def generate_mipmap(self): - """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): + def get_level_data(self, level=0, calc_alpha=False, report=None, indent=2, fast=False): """Gets the uncompressed pixel data for a requested mip level, optionally calculating the alpha channel from the image color data """ - width = self._get_tex_param(bgl.GL_TEXTURE_WIDTH, level) - height = self._get_tex_param(bgl.GL_TEXTURE_HEIGHT, level) + + # 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. Thereofre, 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: - report.msg("Level #{}: {}x{}", level, width, height, indent=2) + report.msg("Level #{}: {}x{}", level, eWidth, eHeight, indent=indent) - # Grab the image data - size = width * height * 4 - buf = bgl.Buffer(bgl.GL_BYTE, size) - fmt = bgl.GL_BGRA if bgra else bgl.GL_RGBA - bgl.glGetTexImage(bgl.GL_TEXTURE_2D, level, fmt, bgl.GL_UNSIGNED_BYTE, buf); + # Scale, if needed... + if oWidth != eWidth or oHeight != eHeight: + buf = _scale_image(self._image_data, oWidth, oHeight, eWidth, eHeight) + else: + 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: - 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) - 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] + + if self._image_inverted: + buf = self._invert_image(eWidth, eHeight, buf) # If this is a detail map, then we need to bake that per-level here. if self._texkey.is_detail_map: detail_blend = self._texkey.detail_blend 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: - self._make_detail_map_alpha(finalBuf, level) + self._make_detail_map_alpha(buf, level) 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? if calc_alpha: for i in range(0, size, 4): - finalBuf[i+3] = int(sum(finalBuf[i:i+3]) / 3) - return bytes(finalBuf) + buf[i+3] = int(sum(buf[i:i+3]) / 3) + return bytes(buf) 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 @@ -138,12 +223,21 @@ class GLTexture: @property 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): if data[i] != 255: return True return False + 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): 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) @@ -167,7 +261,7 @@ class GLTexture: @property 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 # There is a bug in Cyan's level size algorithm that causes it to not allocate enough memory @@ -181,3 +275,11 @@ class GLTexture: # texture in a single pixel?" # :) 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)