From 319b43814f041d9cf5c794ba1473fd37bb033d40 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 29 Jun 2016 22:47:28 -0400 Subject: [PATCH] Implement detail fading for Plasma Layers Yes, this was shamelessly stolen from the max plugin. So shoot me. It's an awesome feature. --- korlib/texture.cpp | 134 ++++++++++++++++++++++++++++-- korman/exporter/material.py | 94 +++++++++++++-------- korman/korlib/__init__.py | 2 + korman/korlib/texture.py | 79 +++++++++++++++++- korman/properties/prop_texture.py | 21 +++++ korman/ui/ui_texture.py | 15 ++++ 6 files changed, 301 insertions(+), 44 deletions(-) diff --git a/korlib/texture.cpp b/korlib/texture.cpp index a69aae5..9224f01 100644 --- a/korlib/texture.cpp +++ b/korlib/texture.cpp @@ -23,6 +23,7 @@ # include #endif // _WIN32 +#include #include #include @@ -32,11 +33,27 @@ #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; @@ -50,13 +67,15 @@ typedef struct { } pyMipmap; static void pyGLTexture_dealloc(pyGLTexture* self) { - if (self->m_blenderImage) Py_DECREF(self->m_blenderImage); + 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; @@ -65,15 +84,17 @@ static PyObject* pyGLTexture_new(PyTypeObject* type, PyObject* args, PyObject* k } static int pyGLTexture___init__(pyGLTexture* self, PyObject* args, PyObject* kwds) { - PyObject* blender_image; - if (!PyArg_ParseTuple(args, "O", &blender_image)) { - PyErr_SetString(PyExc_TypeError, "expected a bpy.types.Image"); + 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; } - // Save a reference to the Blender image - Py_INCREF(blender_image); - self->m_blenderImage = blender_image; + Py_INCREF(self->m_textureKey); // Done! return 0; @@ -161,6 +182,90 @@ struct _LevelData { } }; +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); @@ -206,6 +311,16 @@ static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, P } 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; @@ -268,8 +383,13 @@ static PyObject* pyGLTexture_get_has_alpha(pyGLTexture* self, void*) { 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 } }; diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 33e8963..1e3509c 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -21,12 +21,19 @@ import weakref from . import explosions from .. import helpers -from .. import korlib +from ..korlib import * from . import utils class _Texture: - def __init__(self, texture=None, image=None, use_alpha=None, force_calc_alpha=False): - assert (texture or image) + _DETAIL_BLEND = { + TEX_DETAIL_ALPHA: "AL", + TEX_DETAIL_ADD: "AD", + TEX_DETAIL_MULTIPLY: "ML", + } + + def __init__(self, **kwargs): + texture, image = kwargs.get("texture"), kwargs.get("image") + assert texture or image if texture is not None: if image is None: @@ -34,16 +41,29 @@ class _Texture: self.calc_alpha = texture.use_calculate_alpha self.mipmap = texture.use_mipmap else: + self.layer = kwargs.get("layer") self.calc_alpha = False self.mipmap = False - if force_calc_alpha or self.calc_alpha: - self.calc_alpha = True - self.use_alpha = True - elif use_alpha is None: - self.use_alpha = (image.channels == 4 and image.use_alpha) + if kwargs.get("is_detail_map", False): + self.is_detail_map = True + self.detail_blend = kwargs["detail_blend"] + self.detail_fade_start = kwargs["detail_fade_start"] + self.detail_fade_stop = kwargs["detail_fade_stop"] + self.detail_opacity_start = kwargs["detail_opacity_start"] + self.detail_opacity_stop = kwargs["detail_opacity_stop"] + self.calc_alpha = False + self.use_alpha = True else: - self.use_alpha = use_alpha + self.is_detail_map = False + use_alpha = kwargs.get("use_alpha") + if kwargs.get("force_calc_alpha", False) or self.calc_alpha: + self.calc_alpha = True + self.use_alpha = True + elif use_alpha is None: + self.use_alpha = (image.channels == 4 and image.use_alpha) + else: + self.use_alpha = use_alpha self.image = image @@ -51,13 +71,14 @@ class _Texture: if not isinstance(other, _Texture): return False - if self.image == other.image: - if self.calc_alpha == other.calc_alpha: - self._update(other) - return True + # Yeah, the string name is a unique identifier. So shoot me. + if str(self) == str(other): + self._update(other) + return True + return False def __hash__(self): - return hash(self.image.name) ^ hash(self.calc_alpha) + return hash(str(self)) def __str__(self): if self.mipmap: @@ -66,10 +87,17 @@ class _Texture: name = str(Path(self.image.name).with_suffix(".bmp")) if self.calc_alpha: name = "ALPHAGEN_{}".format(name) + + if self.is_detail_map: + name = "DETAILGEN_{}-{}-{}-{}-{}_{}".format(self._DETAIL_BLEND[self.detail_blend], + self.detail_fade_start, self.detail_fade_stop, + self.detail_opacity_start, self.detail_opacity_stop, + name) return name def _update(self, other): """Update myself with any props that might be overridable from another copy of myself""" + # NOTE: detail map properties should NEVER be overridden. NEVER. EVER. kthx. if other.use_alpha: self.use_alpha = True if other.mipmap: @@ -424,6 +452,7 @@ class MaterialConverter: def _export_texture_type_image(self, bo, layer, slot): """Exports a Blender ImageTexture to a plLayer""" texture = slot.texture + layer_props = texture.plasma_layer # Does the image have any alpha at all? if texture.image is not None: @@ -462,7 +491,17 @@ class MaterialConverter: dtm.visWidth, dtm.visHeight = 1024, 1024 layer.texture = dtm.key else: - key = _Texture(texture=texture, use_alpha=has_alpha, force_calc_alpha=slot.use_stencil) + detail_blend = TEX_DETAIL_ALPHA + if layer_props.is_detail_map and texture.use_mipmap: + if slot.blend_type == "ADD": + detail_blend = TEX_DETAIL_ADD + elif slot.blend_type == "MULTIPLY": + detail_blend = TEX_DETAIL_MULTIPLY + + key = _Texture(texture=texture, use_alpha=has_alpha, force_calc_alpha=slot.use_stencil, + is_detail_map=layer_props.is_detail_map, detail_blend=detail_blend, + detail_fade_start=layer_props.detail_fade_start, detail_fade_stop=layer_props.detail_fade_stop, + detail_opacity_start=layer_props.detail_opacity_start, detail_opacity_stop=layer_props.detail_opacity_stop) if key not in self._pending: print(" Stashing '{}' for conversion as '{}'".format(texture.image.name, str(key))) self._pending[key] = [layer.key,] @@ -481,7 +520,7 @@ class MaterialConverter: print(" Stashing '{}' for conversion as '{}'".format(image.name, str(key))) self._pending[key] = [layer.key,] else: - print(" Found another user of '{}'".format(image.name)) + print(" Found another user of '{}'".format(key)) self._pending[key].append(layer.key) def finalize(self): @@ -498,32 +537,18 @@ class MaterialConverter: self._resize_image(image, eWidth, eHeight) # Some basic mipmap settings. - numLevels = math.floor(math.log(max(eWidth, eHeight), 2)) + 1 if key.mipmap else 1 compression = plBitmap.kDirectXCompression if key.mipmap else plBitmap.kUncompressed dxt = plBitmap.kDXT5 if key.use_alpha or key.calc_alpha else plBitmap.kDXT1 - # 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?" - # :) - if key.mipmap: - # If your mipmap only has 2 levels (or less), then you deserve to phail... - numLevels = max(numLevels - 2, 2) - # Grab the image data from OpenGL and stuff it into the plBitmap - helper = korlib.GLTexture(image) + helper = GLTexture(key) with helper as glimage: if key.mipmap: + numLevels = glimage.num_levels print(" Generating mip levels") glimage.generate_mipmap() else: + numLevels = 1 print(" Stuffing image data") # Uncompressed bitmaps are BGRA @@ -606,7 +631,8 @@ class MaterialConverter: result = False else: # Using bpy.types.Image.pixels is VERY VERY VERY slow... - with korlib.GLTexture(image) as glimage: + key = _Texture(image=image) + with GLTexture(key) as glimage: result = glimage.has_alpha self._alphatest[image] = result diff --git a/korman/korlib/__init__.py b/korman/korlib/__init__.py index 4c3fbe6..9a4c22d 100644 --- a/korman/korlib/__init__.py +++ b/korman/korlib/__init__.py @@ -34,3 +34,5 @@ except ImportError: assert not stream.eof() size = stream.readInt() return (header, size) +else: + from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY diff --git a/korman/korlib/texture.py b/korman/korlib/texture.py index 16e2080..11a289e 100644 --- a/korman/korlib/texture.py +++ b/korman/korlib/texture.py @@ -14,16 +14,26 @@ # along with Korman. If not, see . import bgl +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 +TEX_DETAIL_ALPHA = 0 +TEX_DETAIL_ADD = 1 +TEX_DETAIL_MULTIPLY = 2 + class GLTexture: - def __init__(self, blimg): - self._ownit = (blimg.bindcode[0] == 0) - self._blimg = blimg + def __init__(self, texkey=None): + self._texkey = texkey + self._ownit = (self._blimg.bindcode[0] == 0) + + @property + def _blimg(self): + return self._texkey.image def __enter__(self): """Sets the Blender Image as the active OpenGL texture""" @@ -46,6 +56,14 @@ class GLTexture: if self._ownit: self._blimg.gl_free() + @property + def _detail_falloff(self): + num_levels = self.num_levels + return ((self._texkey.detail_fade_start / 100.0) * num_levels, + (self._texkey.detail_fade_stop / 100.0) * num_levels, + 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) @@ -82,12 +100,29 @@ class GLTexture: 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 self._texkey.is_detail_map: + detail_blend = self._texkey.detail_blend + if detail_blend == TEX_DETAIL_ALPHA: + self._make_detail_map_alpha(finalBuf, level) + elif detail_blend == TEX_DETAIL_ADD: + self._make_detail_map_alpha(finalBuf, level) + elif detail_blend == TEX_DETAIL_MULTIPLY: + self._make_detail_map_mult(finalBuf, 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) + 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 + if detail_min < detail_max: + return min(detail_max, max(detail_min, alpha)) + else: + return min(detail_min, max(detail_max, alpha)) + def _get_integer(self, arg): buf = bgl.Buffer(bgl.GL_INT, 1) bgl.glGetIntegerv(arg, buf) @@ -109,6 +144,44 @@ class GLTexture: return True return False + 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) + for i in range(0, len(data), 4): + data[i] = int(data[i] * alpha) + data[i+1] = int(data[i+1] * alpha) + data[i+2] = int(data[i+2] * alpha) + + def _make_detail_map_alpha(self, data, level): + dropoff_start, dropoff_end, detail_max, detail_min = self._detail_falloff + alpha = self._get_detail_alpha(level, dropoff_start, dropoff_end, detail_max, detail_min) + for i in range(0, len(data), 4): + data[i+3] = int(data[i+3] * alpha) + + def _make_detail_map_mult(self, data, level): + dropoff_start, dropoff_end, detail_max, detail_min = self._detail_falloff + alpha = self._get_detail_alpha(level, dropoff_start, dropoff_end, detail_max, detail_min) + invert_alpha = (1.0 - alpha) * 255.0 + for i in range(0, len(data), 4): + data[i+3] = int(invert_alpha + data[i+3] * alpha) + + @property + def num_levels(self): + numLevels = math.floor(math.log(max(self._blimg.size), 2)) + 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 max(numLevels - 2, 2) + def store_in_mipmap(self, mipmap, data, compression): func = mipmap.CompressImage if compression == plBitmap.kDirectXCompression else mipmap.setLevel for i, level in enumerate(data): diff --git a/korman/properties/prop_texture.py b/korman/properties/prop_texture.py index 7979881..6f24778 100644 --- a/korman/properties/prop_texture.py +++ b/korman/properties/prop_texture.py @@ -52,3 +52,24 @@ class PlasmaLayer(bpy.types.PropertyGroup): anim_loop = BoolProperty(name="Loop", description="Loop layer animation", default=True) + + is_detail_map = BoolProperty(name="Detail Fade", + description="Texture fades out as distance from the camera increases", + default=False, + options=set()) + detail_fade_start = IntProperty(name="Falloff Start", + description="", + min=0, max=100, default=0, + options=set(), subtype="PERCENTAGE") + detail_fade_stop = IntProperty(name="Falloff Stop", + description="", + min=0, max=100, default=100, + options=set(), subtype="PERCENTAGE") + detail_opacity_start = IntProperty(name="Opacity Start", + description="", + min=0, max=100, default=50, + options=set(), subtype="PERCENTAGE") + detail_opacity_stop = IntProperty(name="Opacity Stop", + description="", + min=0, max=100, default=0, + options=set(), subtype="PERCENTAGE") diff --git a/korman/ui/ui_texture.py b/korman/ui/ui_texture.py index b5613ba..afaa99b 100644 --- a/korman/ui/ui_texture.py +++ b/korman/ui/ui_texture.py @@ -66,6 +66,21 @@ class PlasmaLayerPanel(TextureButtonsPanel, bpy.types.Panel): layer_props = texture.plasma_layer layout = self.layout + col = layout.column() + col.active = texture.use_mipmap + col.prop(layer_props, "is_detail_map", text="Use Detail Blending") + + split = layout.split() + col = split.column(align=True) + col.active = texture.use_mipmap and layer_props.is_detail_map + col.prop(layer_props, "detail_fade_start") + col.prop(layer_props, "detail_fade_stop") + col = split.column(align=True) + col.active = texture.use_mipmap and layer_props.is_detail_map + col.prop(layer_props, "detail_opacity_start") + col.prop(layer_props, "detail_opacity_stop") + layout.separator() + split = layout.split() col = split.column() col.label("Animation:")