Browse Source

Implement cubemap generate operator

This operator takes a file as an argument and builds a cubemap from it.
Valid options are to supply the output from Plasma's
Graphics.Renderer.GrabCubeMap console command. The operator will find
the other five files and generate a cubemap with the faces saved by
Plasma. Otherwise, any arbitrary image can be supplied. If the filenames
do not fit the expected format, any missing faces will be replaced by
the face specified in the file selector. This will generally result in a
cubemap with six identical faces.
pull/125/head
Adam Johnson 6 years ago
parent
commit
151c31f2e9
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 1
      korlib/korlib.h
  2. 25
      korlib/texture.cpp
  3. 8
      korman/exporter/material.py
  4. 2
      korman/helpers.py
  5. 13
      korman/korlib/texture.py
  6. 1
      korman/operators/__init__.py
  7. 237
      korman/operators/op_image.py
  8. 9
      korman/ui/ui_texture.py

1
korlib/korlib.h

@ -29,6 +29,7 @@ class PyObjectRef {
PyObject* m_object; PyObject* m_object;
public: public:
PyObjectRef() : m_object() { }
PyObjectRef(PyObject* o) : m_object(o) { } PyObjectRef(PyObject* o) : m_object(o) { }
~PyObjectRef() { Py_XDECREF(m_object); } ~PyObjectRef() { Py_XDECREF(m_object); }

25
korlib/texture.cpp

@ -63,11 +63,13 @@ static void _flip_image(size_t width, size_t dataSize, uint8_t* data) {
} }
static inline bool _get_float(PyObject* source, const char* attr, float& result) { static inline bool _get_float(PyObject* source, const char* attr, float& result) {
if (source) {
PyObjectRef pyfloat = PyObject_GetAttrString(source, attr); PyObjectRef pyfloat = PyObject_GetAttrString(source, attr);
if (pyfloat) { if (pyfloat) {
result = (float)PyFloat_AsDouble(pyfloat); result = (float)PyFloat_AsDouble(pyfloat);
return PyErr_Occurred() == NULL; return PyErr_Occurred() == NULL;
} }
}
return false; return false;
} }
@ -231,20 +233,27 @@ static PyObject* pyGLTexture_new(PyTypeObject* type, PyObject* args, PyObject* k
} }
static int pyGLTexture___init__(pyGLTexture* self, PyObject* args, PyObject* kwds) { static int pyGLTexture___init__(pyGLTexture* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = { _pycs("texkey"), _pycs("bgra"), _pycs("fast"), NULL }; static char* kwlist[] = { _pycs("texkey"), _pycs("image"), _pycs("bgra"), _pycs("fast"), NULL };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|bb", kwlist, &self->m_textureKey, if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OObb", kwlist, &self->m_textureKey, &self->m_blenderImage,
&self->m_bgra, &self->m_imageInverted)) { &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 or a bpy.types.Image");
return -1;
}
if (!self->m_blenderImage && !self->m_textureKey) {
PyErr_SetString(PyExc_TypeError, "expected a korman.exporter.material._Texture or a bpy.types.Image");
return -1; return -1;
} }
Py_XINCREF(self->m_blenderImage);
Py_XINCREF(self->m_textureKey);
if (!self->m_blenderImage) {
self->m_blenderImage = PyObject_GetAttrString(self->m_textureKey, "image"); self->m_blenderImage = PyObject_GetAttrString(self->m_textureKey, "image");
}
if (!self->m_blenderImage) { if (!self->m_blenderImage) {
PyErr_SetString(PyExc_RuntimeError, "Could not fetch Blender Image"); PyErr_SetString(PyExc_RuntimeError, "Could not fetch Blender Image");
return -1; return -1;
} }
Py_INCREF(self->m_textureKey);
// Done! // Done!
return 0; return 0;
} }
@ -358,7 +367,9 @@ static int _generate_detail_map(pyGLTexture* self, uint8_t* buf, size_t bufsz, G
float alpha; float alpha;
if (_generate_detail_alpha(self, level, &alpha) != 0) if (_generate_detail_alpha(self, level, &alpha) != 0)
return -1; return -1;
PyObjectRef pydetail_blend = PyObject_GetAttrString(self->m_textureKey, "detail_blend"); PyObjectRef pydetail_blend;
if (self->m_textureKey)
pydetail_blend = PyObject_GetAttrString(self->m_textureKey, "detail_blend");
if (!pydetail_blend) if (!pydetail_blend)
return -1; return -1;
@ -436,6 +447,7 @@ static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, P
} }
// Detail blend // Detail blend
if (self->m_textureKey) {
PyObjectRef is_detail_map = PyObject_GetAttrString(self->m_textureKey, "is_detail_map"); PyObjectRef is_detail_map = PyObject_GetAttrString(self->m_textureKey, "is_detail_map");
if (PyLong_AsLong(is_detail_map) != 0) { if (PyLong_AsLong(is_detail_map) != 0) {
_ensure_copy_bytes(self->m_imageData, data); _ensure_copy_bytes(self->m_imageData, data);
@ -446,6 +458,7 @@ static PyObject* pyGLTexture_get_level_data(pyGLTexture* self, PyObject* args, P
return NULL; return NULL;
} }
} }
}
if (calc_alpha) { if (calc_alpha) {
_ensure_copy_bytes(self->m_imageData, data); _ensure_copy_bytes(self->m_imageData, data);

8
korman/exporter/material.py

@ -28,7 +28,7 @@ _MAX_STENCILS = 6
# Blender cube map mega image to libHSPlasma plCubicEnvironmap faces mapping... # Blender cube map mega image to libHSPlasma plCubicEnvironmap faces mapping...
# See https://blender.stackexchange.com/questions/46891/how-to-render-an-environment-to-a-cube-map-in-cycles # See https://blender.stackexchange.com/questions/46891/how-to-render-an-environment-to-a-cube-map-in-cycles
_BLENDER_CUBE_MAP = ("leftFace", "backFace", "rightFace", BLENDER_CUBE_MAP = ("leftFace", "backFace", "rightFace",
"bottomFace", "topFace", "frontFace") "bottomFace", "topFace", "frontFace")
class _Texture: class _Texture:
@ -810,7 +810,7 @@ class MaterialConverter:
if key.is_cube_map: if key.is_cube_map:
assert len(data) == 6 assert len(data) == 6
texture = plCubicEnvironmap(name) texture = plCubicEnvironmap(name)
for face_name, face_data in zip(_BLENDER_CUBE_MAP, data): for face_name, face_data in zip(BLENDER_CUBE_MAP, data):
for i in range(numLevels): for i in range(numLevels):
mipmap.setLevel(i, face_data[i]) mipmap.setLevel(i, face_data[i])
setattr(texture, face_name, mipmap) setattr(texture, face_name, mipmap)
@ -866,7 +866,7 @@ class MaterialConverter:
# According to my profiling, it takes roughly 0.7 seconds to process a # According to my profiling, it takes roughly 0.7 seconds to process a
# cube map whose faces are 1024x1024 (3072x2048 total). Maybe a later # cube map whose faces are 1024x1024 (3072x2048 total). Maybe a later
# commit will move this into korlib. We'll see. # commit will move this into korlib. We'll see.
face_num = len(_BLENDER_CUBE_MAP) face_num = len(BLENDER_CUBE_MAP)
face_images = [None] * face_num face_images = [None] * face_num
for i in range(face_num): for i in range(face_num):
col_id = i if i < 3 else i - 3 col_id = i if i < 3 else i - 3
@ -884,7 +884,7 @@ class MaterialConverter:
# Now that we have our six faces, we'll toss them into the GLTexture helper # Now that we have our six faces, we'll toss them into the GLTexture helper
# to generate mipmaps, if needed... # to generate mipmaps, if needed...
for i, face_name in enumerate(_BLENDER_CUBE_MAP): for i, face_name in enumerate(BLENDER_CUBE_MAP):
glimage = GLTexture(key) glimage = GLTexture(key)
glimage.image_data = fWidth, fHeight, face_images[i] glimage.image_data = fWidth, fHeight, face_images[i]
eWidth, eHeight = glimage.size_pot eWidth, eHeight = glimage.size_pot

2
korman/helpers.py

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

13
korman/korlib/texture.py

@ -96,15 +96,16 @@ def scale_image(buf, srcW, srcH, dstW, dstH):
class GLTexture: class GLTexture:
def __init__(self, texkey=None, bgra=False, fast=False): def __init__(self, texkey=None, image=None, bgra=False, fast=False):
assert texkey or image
self._texkey = texkey self._texkey = texkey
if texkey is not None:
self._blimg = texkey.image
if image is not None:
self._blimg = image
self._image_inverted = fast self._image_inverted = fast
self._bgra = bgra self._bgra = bgra
@property
def _blimg(self):
return self._texkey.image
def __enter__(self): def __enter__(self):
"""Loads the image data using OpenGL""" """Loads the image data using OpenGL"""
@ -186,7 +187,7 @@ class GLTexture:
buf = self._invert_image(eWidth, eHeight, buf) buf = self._invert_image(eWidth, eHeight, buf)
# If this is a detail map, then we need to bake that per-level here. # If this is a detail map, then we need to bake that per-level here.
if self._texkey.is_detail_map: if self._texkey is not None and self._texkey.is_detail_map:
detail_blend = self._texkey.detail_blend detail_blend = self._texkey.detail_blend
if detail_blend == TEX_DETAIL_ALPHA: if detail_blend == TEX_DETAIL_ALPHA:
self._make_detail_map_alpha(buf, level) self._make_detail_map_alpha(buf, level)

1
korman/operators/__init__.py

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

237
korman/operators/op_image.py

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

9
korman/ui/ui_texture.py

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

Loading…
Cancel
Save