mirror of
https://github.com/H-uru/korman.git
synced 2025-07-14 02:27:36 -04:00
Refactor image data handling for cube maps
Previously, we allowed OpenGL to generate all of the mip levels for us in a mipmap. This was pretty doggone fast and worked reasonably well. However, with cube maps, we will need to use images that are not always backed in Blender... this is because Blender stores cube maps as one single image instead of one image per face. So, we need to be able to generate those mip levels, preferably without touching Blender's `Image.pixels`, which is slower than Christmas... Also of note... `Image.gl_load()` will actually scale the iamge to a POT when Blender is using OpenGL ES... but not on other platforms. So, now, we just ask Blender to load the image and deal with the POT-izing later. The con here is that the pure python implementation of the image scaling function is SLOOOOOOOW. We're talking ~40 seconds to process a 1024x1024 mipmap. No one should be using the reference implementation, however, and the C++ implementation shows no noticable slowdown over the OpenGL code. Whew.
This commit is contained in:
@ -23,6 +23,7 @@
|
|||||||
#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;
|
||||||
|
@ -26,12 +26,40 @@
|
|||||||
#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) {
|
||||||
PyObjectRef pyfloat = PyObject_GetAttrString(source, attr);
|
PyObjectRef pyfloat = PyObject_GetAttrString(source, attr);
|
||||||
if (pyfloat) {
|
if (pyfloat) {
|
||||||
@ -41,7 +69,93 @@ static inline bool _get_float(PyObject* source, const char* attr, float& result)
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
TEX_DETAIL_ALPHA = 0,
|
TEX_DETAIL_ALPHA = 0,
|
||||||
@ -53,10 +167,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 {
|
||||||
@ -66,8 +181,9 @@ typedef struct {
|
|||||||
} 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,15 +191,18 @@ 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("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");
|
PyErr_SetString(PyExc_TypeError, "expected a korman.exporter.material._Texture");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -115,12 +234,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 +264,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;
|
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.
|
|
||||||
// "<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 +309,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;
|
||||||
|
|
||||||
@ -265,102 +360,110 @@ 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 :(
|
||||||
static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, PyObject* kwargs) {
|
uint8_t* srcBuf = reinterpret_cast<uint8_t*>(PyBytes_AsString(self->m_imageData));
|
||||||
static char* kwlist[] = { _pycs("level"), _pycs("calc_alpha"), _pycs("bgra"),
|
_scale_image(srcBuf, self->m_width, self->m_height, dstBuf, eWidth, eHeight);
|
||||||
_pycs("report"), _pycs("fast"), NULL };
|
|
||||||
GLint level = 0;
|
|
||||||
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);
|
// Make sure the level data is not flipped upside down...
|
||||||
if (fast)
|
if (self->m_imageInverted && !fast) {
|
||||||
return PyBytes_FromStringAndSize((char*)data.m_data, data.m_dataSize);
|
_ensure_copy_bytes(self->m_blenderImage, data);
|
||||||
|
_flip_image(eWidth, bufsz, reinterpret_cast<uint8_t*>(PyBytes_AS_STRING(data)));
|
||||||
// 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
|
||||||
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_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("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 +532,3 @@ PyObject* Init_pyGLTexture_Type() {
|
|||||||
Py_INCREF(&pyGLTexture_Type);
|
Py_INCREF(&pyGLTexture_Type);
|
||||||
return (PyObject*)&pyGLTexture_Type;
|
return (PyObject*)&pyGLTexture_Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@ -726,9 +726,6 @@ class MaterialConverter:
|
|||||||
self._report.msg("\n[Mipmap '{}']", name)
|
self._report.msg("\n[Mipmap '{}']", 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
|
||||||
@ -750,48 +747,11 @@ 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)
|
numLevels, width, height, data = self._finalize_single_image(key, image, name, compression, dxt)
|
||||||
eHeight = helpers.ensure_power_of_two(oHeight)
|
texcache.add_texture(key, numLevels, (width, height), compression, data)
|
||||||
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,])
|
|
||||||
else:
|
else:
|
||||||
eWidth, eHeight = cached_image.export_size
|
width, height = cached_image.export_size
|
||||||
data = cached_image.image_data[0]
|
data = cached_image.image_data
|
||||||
numLevels = cached_image.mip_levels
|
numLevels = cached_image.mip_levels
|
||||||
|
|
||||||
# Now we poke our new bitmap into the pending layers. Note that we have to do some funny
|
# 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)
|
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,
|
||||||
compType=compression, format=plBitmap.kRGB8888, dxtLevel=dxt)
|
numLevels=numLevels, compType=compression,
|
||||||
for i, buf in enumerate(data):
|
format=plBitmap.kRGB8888, dxtLevel=dxt)
|
||||||
mipmap.setLevel(i, buf)
|
for i in range(numLevels):
|
||||||
|
mipmap.setLevel(i, data[0][i])
|
||||||
mgr.AddObject(page, mipmap)
|
mgr.AddObject(page, mipmap)
|
||||||
pages[page] = mipmap
|
pages[page] = mipmap
|
||||||
else:
|
else:
|
||||||
mipmap = pages[page]
|
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):
|
if isinstance(owner, plLayerInterface):
|
||||||
owner.texture = mipmap.key
|
owner.texture = mipmap.key
|
||||||
elif isinstance(owner, plImageLibMod):
|
elif isinstance(owner, plImageLibMod):
|
||||||
@ -826,6 +790,40 @@ class MaterialConverter:
|
|||||||
|
|
||||||
inc_progress()
|
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):
|
def get_materials(self, bo):
|
||||||
return self._obj2mat.get(bo, [])
|
return self._obj2mat.get(bo, [])
|
||||||
|
|
||||||
@ -882,15 +880,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"""
|
||||||
|
|
||||||
@ -906,7 +895,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
|
||||||
|
@ -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,121 @@ 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, bgra=False, fast=False):
|
||||||
self._texkey = texkey
|
self._texkey = texkey
|
||||||
self._ownit = (self._blimg.bindcode[0] == 0)
|
self._image_inverted = fast
|
||||||
|
self._bgra = bgra
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _blimg(self):
|
def _blimg(self):
|
||||||
return self._texkey.image
|
return self._texkey.image
|
||||||
|
|
||||||
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 +151,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. 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:
|
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_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 +223,21 @@ 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 _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 +261,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 +275,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)
|
||||||
|
Reference in New Issue
Block a user