/* This file is part of Korman. * * Korman is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Korman is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Korman. If not, see . */ #include "texture.h" #include "buffer.h" #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # define NOMINMAX # include #endif // _WIN32 #include #include #include #ifndef GL_GENERATE_MIPMAP # define GL_GENERATE_MIPMAP 0x8191 #endif // GL_GENERATE_MIPMAP #define TEXTARGET_TEXTURE_2D 0 static inline bool _get_float(PyObject* source, const char* attr, float& result) { PyObjectRef pyfloat = PyObject_GetAttrString(source, attr); if (pyfloat) { result = (float)PyFloat_AsDouble(pyfloat); return PyErr_Occurred() == NULL; } return false; } extern "C" { enum { TEX_DETAIL_ALPHA = 0, TEX_DETAIL_ADD = 1, TEX_DETAIL_MULTIPLY = 2, }; typedef struct { PyObject_HEAD PyObject* m_blenderImage; PyObject* m_textureKey; bool m_ownIt; GLint m_prevImage; bool m_changedState; GLint m_mipmapState; } pyGLTexture; typedef struct { PyObject_HEAD plMipmap* fThis; bool fPyOwned; } pyMipmap; static void pyGLTexture_dealloc(pyGLTexture* self) { Py_XDECREF(self->m_textureKey); Py_XDECREF(self->m_blenderImage); Py_TYPE(self)->tp_free((PyObject*)self); } static PyObject* pyGLTexture_new(PyTypeObject* type, PyObject* args, PyObject* kwds) { 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; return (PyObject*)self; } static int pyGLTexture___init__(pyGLTexture* self, PyObject* args, PyObject* kwds) { if (!PyArg_ParseTuple(args, "O", &self->m_textureKey)) { PyErr_SetString(PyExc_TypeError, "expected a korman.exporter.material._Texture"); return -1; } self->m_blenderImage = PyObject_GetAttrString(self->m_textureKey, "image"); if (!self->m_blenderImage) { PyErr_SetString(PyExc_RuntimeError, "Could not fetch Blender Image"); return -1; } Py_INCREF(self->m_textureKey); // Done! return 0; } static PyObject* pyGLTexture__enter__(pyGLTexture* self) { // Is the image already loaded? PyObjectRef bindcode = PyObject_GetAttrString(self->m_blenderImage, "bindcode"); // bindcode changed to a sequence in 2.77. We want the first element for a 2D texture. // Why did we make this change, exactly? if (PySequence_Check(bindcode)) { bindcode = PySequence_GetItem(bindcode, TEXTARGET_TEXTURE_2D); } // Now we should have a GLuint... if (!PyLong_Check(bindcode)) { PyErr_SetString(PyExc_TypeError, "Image bindcode isn't a long?"); return NULL; } glGetIntegerv(GL_TEXTURE_BINDING_2D, &self->m_prevImage); GLuint image_bindcode = PyLong_AsUnsignedLong(bindcode); self->m_ownIt = image_bindcode == 0; // Load image into GL if (self->m_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"); return NULL; } ssize_t result = PyLong_AsSize_t(new_bind); if (result != GL_NO_ERROR) { PyErr_Format(PyExc_RuntimeError, "gl_load() error: %d", result); return NULL; } bindcode = PyObject_GetAttrString(self->m_blenderImage, "bindcode"); if (PySequence_Check(bindcode)) { bindcode = PySequence_GetItem(bindcode, TEXTARGET_TEXTURE_2D); } // Now we should have a GLuint... if (!PyLong_Check(bindcode)) { PyErr_SetString(PyExc_TypeError, "Image bindcode isn't a long?"); return NULL; } image_bindcode = PyLong_AsUnsignedLong(bindcode); } // Set image as current in GL if (self->m_prevImage != image_bindcode) { self->m_changedState = true; glBindTexture(GL_TEXTURE_2D, image_bindcode); } // Misc GL state glGetTexParameteriv(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, &self->m_mipmapState); 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_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)) return -1; if (!_get_float(self->m_textureKey, "detail_fade_stop", dropoff_stop)) return -1; if (!_get_float(self->m_textureKey, "detail_opacity_start", detail_max)) return -1; if (!_get_float(self->m_textureKey, "detail_opacity_stop", detail_min)) return -1; dropoff_start /= 100.f; dropoff_start *= _get_num_levels(self); dropoff_stop /= 100.f; dropoff_stop *= _get_num_levels(self); detail_max /= 100.f; detail_min /= 100.f; float alpha = (level - dropoff_start) * (detail_min - detail_max) / (dropoff_stop - dropoff_start) + detail_max; if (detail_min < detail_max) *result = std::min(detail_max, std::max(detail_min, alpha)); else *result = std::min(detail_min, std::max(detail_max, alpha)); return 0; } static int _generate_detail_map(pyGLTexture* self, uint8_t* buf, size_t bufsz, GLint level) { float alpha; if (_generate_detail_alpha(self, level, &alpha) != 0) return -1; PyObjectRef pydetail_blend = PyObject_GetAttrString(self->m_textureKey, "detail_blend"); if (!pydetail_blend) return -1; size_t detail_blend = PyLong_AsSize_t(pydetail_blend); switch (detail_blend) { case TEX_DETAIL_ALPHA: { for (size_t i = 0; i < bufsz; i += 4) { buf[i+3] = (uint8_t)(((float)buf[i+3]) * alpha); } } break; case TEX_DETAIL_ADD: { for (size_t i = 0; i < bufsz; i += 4) { buf[i+0] = (uint8_t)(((float)buf[i+0]) * alpha); buf[i+1] = (uint8_t)(((float)buf[i+1]) * alpha); buf[i+2] = (uint8_t)(((float)buf[i+2]) * alpha); } } break; case TEX_DETAIL_MULTIPLY: { float invert_alpha = (1.f - alpha) * 255.f; for (size_t i = 0; i < bufsz; i += 4) { buf[i+3] = (uint8_t)((invert_alpha + (float)buf[i+3]) * alpha); } } break; default: return -1; } return 0; } static _LevelData _get_level_data(pyGLTexture* self, GLint level, bool bgra, bool quiet) { GLint width, height; glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width); glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height); GLenum fmt = bgra ? GL_BGRA_EXT : GL_RGBA; if (!quiet) PySys_WriteStdout(" Level #%i: %ix%i\n", level, width, height); 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("quiet"), _pycs("fast"), NULL }; GLint level = 0; bool calc_alpha = false; bool bgra = false; bool quiet = false; bool fast = false; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ibbbb", kwlist, &level, &calc_alpha, &bgra, &quiet, &fast)) { PyErr_SetString(PyExc_TypeError, "get_level_data expects an optional int, bool, bool, bool, bool"); return NULL; } _LevelData data = _get_level_data(self, level, bgra, quiet); if (fast) return pyBuffer_Steal(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 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; PyErr_SetString(PyExc_RuntimeError, "error while baking detail map"); 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; } return pyBuffer_Steal(data.m_data, data.m_dataSize); } static PyObject* pyGLTexture_store_in_mipmap(pyGLTexture* self, PyObject* args) { pyMipmap* pymipmap; PyObject* levels; size_t compression; if (!PyArg_ParseTuple(args, "OOn", &pymipmap, &levels, &compression) || !PySequence_Check(levels)) { PyErr_SetString(PyExc_TypeError, "store_in_mipmap expects a plMipmap, sequence of Buffer and int"); return NULL; } // Since we actually have no way of knowing if that really is a pyMipmap... plMipmap* mipmap = plMipmap::Convert(pymipmap->fThis, false); if (!mipmap) { PyErr_SetString(PyExc_TypeError, "store_in_mipmap expects a plMipmap, sequence of Buffer and int"); return NULL; } for (Py_ssize_t i = 0; i < PySequence_Size(levels); ++i) { pyBuffer* item = (pyBuffer*)PySequence_GetItem(levels, i); if (!pyBuffer_Check((PyObject*)item)) { PyErr_SetString(PyExc_TypeError, "store_in_mipmap expects a plMipmap, sequence of Buffer and int"); return NULL; } if (compression == plBitmap::kDirectXCompression) mipmap->CompressImage(i, item->m_buffer, item->m_size); else mipmap->setLevelData(i, item->m_buffer, item->m_size); } Py_RETURN_NONE; } static PyMethodDef pyGLTexture_Methods[] = { { _pycs("__enter__"), (PyCFunction)pyGLTexture__enter__, METH_NOARGS, NULL }, { _pycs("__exit__"), (PyCFunction)pyGLTexture__enter__, 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("store_in_mipmap"), (PyCFunction)pyGLTexture_store_in_mipmap, METH_VARARGS, NULL }, { NULL, NULL, 0, NULL } }; static PyObject* pyGLTexture_get_has_alpha(pyGLTexture* self, void*) { _LevelData data = _get_level_data(self, 0, false, true); for (size_t i = 3; i < data.m_dataSize; i += 4) { if (data.m_data[i] != 255) { delete[] data.m_data; 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)); } 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 }, { NULL, NULL, NULL, NULL, NULL } }; PyTypeObject pyGLTexture_Type = { PyVarObject_HEAD_INIT(NULL, 0) "_korlib.GLTexture", /* tp_name */ sizeof(pyGLTexture), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)pyGLTexture_dealloc, /* tp_dealloc */ NULL, /* tp_print */ NULL, /* tp_getattr */ NULL, /* tp_setattr */ NULL, /* tp_compare */ NULL, /* tp_repr */ NULL, /* tp_as_number */ NULL, /* tp_as_sequence */ NULL, /* tp_as_mapping */ NULL, /* tp_hash */ NULL, /* tp_call */ NULL, /* tp_str */ NULL, /* tp_getattro */ NULL, /* tp_setattro */ NULL, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "GLTexture", /* tp_doc */ NULL, /* tp_traverse */ NULL, /* tp_clear */ NULL, /* tp_richcompare */ 0, /* tp_weaklistoffset */ NULL, /* tp_iter */ NULL, /* tp_iternext */ pyGLTexture_Methods, /* tp_methods */ NULL, /* tp_members */ pyGLTexture_GetSet, /* tp_getset */ NULL, /* tp_base */ NULL, /* tp_dict */ NULL, /* tp_descr_get */ NULL, /* tp_descr_set */ 0, /* tp_dictoffset */ (initproc)pyGLTexture___init__, /* tp_init */ NULL, /* tp_alloc */ pyGLTexture_new, /* tp_new */ NULL, /* tp_free */ NULL, /* tp_is_gc */ NULL, /* tp_bases */ NULL, /* tp_mro */ NULL, /* tp_cache */ NULL, /* tp_subclasses */ NULL, /* tp_weaklist */ NULL, /* tp_del */ 0, /* tp_version_tag */ NULL, /* tp_finalize */ }; PyObject* Init_pyGLTexture_Type() { if (PyType_Ready(&pyGLTexture_Type) < 0) return NULL; Py_INCREF(&pyGLTexture_Type); return (PyObject*)&pyGLTexture_Type; } };