glTF exporter: perf: use foreach_{get,set} for image encoding
We can now remove compositor code, no more needed
This commit is contained in:
parent
8dd0687a67
commit
dfadb306b0
|
@ -15,8 +15,8 @@
|
|||
bl_info = {
|
||||
'name': 'glTF 2.0 format',
|
||||
'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
||||
"version": (1, 2, 62),
|
||||
'blender': (2, 82, 7),
|
||||
"version": (1, 2, 63),
|
||||
'blender': (2, 83, 9),
|
||||
'location': 'File > Import-Export',
|
||||
'description': 'Import-Export as glTF 2.0',
|
||||
'warning': '',
|
||||
|
|
|
@ -60,8 +60,6 @@ def gather_primitives(
|
|||
material = gltf2_blender_gather_materials.gather_material(blender_material,
|
||||
double_sided,
|
||||
export_settings)
|
||||
# NOTE: gather_material may invalidate blender_mesh (see #932),
|
||||
# so make sure not to access blender_mesh again after this point
|
||||
except IndexError:
|
||||
# no material at that index
|
||||
pass
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import bpy
|
||||
import os
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
import numpy as np
|
||||
import tempfile
|
||||
import enum
|
||||
|
@ -121,98 +121,8 @@ class ExportImage:
|
|||
return self.__encode_from_image(self.blender_image())
|
||||
|
||||
def __encode_unhappy(self) -> bytes:
|
||||
result = self.__encode_unhappy_with_compositor()
|
||||
if result is not None:
|
||||
return result
|
||||
return self.__encode_unhappy_with_numpy()
|
||||
|
||||
def __encode_unhappy_with_compositor(self) -> bytes:
|
||||
# Builds a Compositor graph that will build the correct image
|
||||
# from the description in self.fills.
|
||||
#
|
||||
# [ Image ]->[ Sep RGBA ] [ Comb RGBA ]
|
||||
# [ src_chan]--->[dst_chan ]--->[ Output ]
|
||||
#
|
||||
# This is hacky, but is about 4x faster than using
|
||||
# __encode_unhappy_with_numpy. There are some caveats though:
|
||||
|
||||
# First, we can't handle pre-multiplied alpha.
|
||||
if Channel.A in self.fills:
|
||||
return None
|
||||
|
||||
# Second, in order to get the same results as using image.pixels
|
||||
# (which ignores the colorspace), we need to use the 'Non-Color'
|
||||
# colorspace for all images and set the output device to 'None'. But
|
||||
# setting the colorspace on dirty images discards their changes.
|
||||
# So we can't handle dirty images that aren't already 'Non-Color'.
|
||||
for fill in self.fills:
|
||||
if isinstance(fill, FillImage):
|
||||
if fill.image.is_dirty:
|
||||
if fill.image.colorspace_settings.name != 'Non-Color':
|
||||
return None
|
||||
|
||||
tmp_scene = None
|
||||
orig_colorspaces = {} # remembers original colorspaces
|
||||
try:
|
||||
tmp_scene = bpy.data.scenes.new('##gltf-export:tmp-scene##')
|
||||
tmp_scene.use_nodes = True
|
||||
node_tree = tmp_scene.node_tree
|
||||
for node in node_tree.nodes:
|
||||
node_tree.nodes.remove(node)
|
||||
|
||||
out = node_tree.nodes.new('CompositorNodeComposite')
|
||||
comb_rgba = node_tree.nodes.new('CompositorNodeCombRGBA')
|
||||
for i in range(4):
|
||||
comb_rgba.inputs[i].default_value = 1.0
|
||||
node_tree.links.new(out.inputs['Image'], comb_rgba.outputs['Image'])
|
||||
|
||||
img_size = None
|
||||
for dst_chan, fill in self.fills.items():
|
||||
if not isinstance(fill, FillImage):
|
||||
continue
|
||||
|
||||
img = node_tree.nodes.new('CompositorNodeImage')
|
||||
img.image = fill.image
|
||||
sep_rgba = node_tree.nodes.new('CompositorNodeSepRGBA')
|
||||
node_tree.links.new(sep_rgba.inputs['Image'], img.outputs['Image'])
|
||||
node_tree.links.new(comb_rgba.inputs[dst_chan], sep_rgba.outputs[fill.src_chan])
|
||||
|
||||
if fill.image.colorspace_settings.name != 'Non-Color':
|
||||
if fill.image.name not in orig_colorspaces:
|
||||
orig_colorspaces[fill.image.name] = \
|
||||
fill.image.colorspace_settings.name
|
||||
fill.image.colorspace_settings.name = 'Non-Color'
|
||||
|
||||
if img_size is None:
|
||||
img_size = fill.image.size[:2]
|
||||
else:
|
||||
# All images should be the same size (should be
|
||||
# guaranteed by gather_texture_info)
|
||||
assert img_size == fill.image.size[:2]
|
||||
|
||||
width, height = img_size or (1, 1)
|
||||
return _render_temp_scene(
|
||||
tmp_scene=tmp_scene,
|
||||
width=width,
|
||||
height=height,
|
||||
file_format=self.file_format,
|
||||
color_mode='RGB',
|
||||
colorspace='None',
|
||||
)
|
||||
|
||||
finally:
|
||||
for img_name, colorspace in orig_colorspaces.items():
|
||||
bpy.data.images[img_name].colorspace_settings.name = colorspace
|
||||
|
||||
if tmp_scene is not None:
|
||||
bpy.data.scenes.remove(tmp_scene, do_unlink=True)
|
||||
|
||||
|
||||
def __encode_unhappy_with_numpy(self):
|
||||
# Read the pixels of each image with image.pixels, put them into a
|
||||
# numpy, and assemble the desired image that way. This is the slowest
|
||||
# method, and the conversion to Python data eats a lot of memory, so
|
||||
# it's only used as a last resort.
|
||||
# We need to assemble the image out of channels.
|
||||
# Do it with numpy and image.pixels.
|
||||
result = None
|
||||
|
||||
img_fills = {
|
||||
|
@ -227,42 +137,40 @@ class ExportImage:
|
|||
image = bpy.data.images[image_name]
|
||||
|
||||
if result is None:
|
||||
result = np.ones((image.size[0], image.size[1], 4), np.float32)
|
||||
dim = (image.size[0], image.size[1])
|
||||
result = np.ones(dim[0] * dim[1] * 4, np.float32)
|
||||
tmp_buf = np.empty(dim[0] * dim[1] * 4, np.float32)
|
||||
# Images should all be the same size (should be guaranteed by
|
||||
# gather_texture_info).
|
||||
assert (image.size[0], image.size[1]) == result.shape[:2]
|
||||
assert (image.size[0], image.size[1]) == dim
|
||||
|
||||
# Slow and eats all your memory.
|
||||
pixels = np.array(image.pixels[:])
|
||||
|
||||
pixels = pixels.reshape((image.size[0], image.size[1], image.channels))
|
||||
image.pixels.foreach_get(tmp_buf)
|
||||
|
||||
for dst_chan, img_fill in img_fills.items():
|
||||
if img_fill.image == image:
|
||||
result[:, :, dst_chan] = pixels[:, :, img_fill.src_chan]
|
||||
result[int(dst_chan)::4] = tmp_buf[int(img_fill.src_chan)::4]
|
||||
|
||||
pixels = None # GC this please
|
||||
tmp_buf = None # GC this
|
||||
|
||||
if result is None:
|
||||
# No ImageFills; use a 1x1 white pixel
|
||||
dim = (1, 1)
|
||||
result = np.array([1.0, 1.0, 1.0, 1.0])
|
||||
result = result.reshape((1, 1, 4))
|
||||
|
||||
return self.__encode_from_numpy_array(result)
|
||||
return self.__encode_from_numpy_array(result, dim)
|
||||
|
||||
def __encode_from_numpy_array(self, array: np.ndarray) -> bytes:
|
||||
def __encode_from_numpy_array(self, pixels: np.ndarray, dim: Tuple[int, int]) -> bytes:
|
||||
tmp_image = None
|
||||
try:
|
||||
tmp_image = bpy.data.images.new(
|
||||
"##gltf-export:tmp-image##",
|
||||
width=array.shape[0],
|
||||
height=array.shape[1],
|
||||
width=dim[0],
|
||||
height=dim[1],
|
||||
alpha=Channel.A in self.fills,
|
||||
)
|
||||
assert tmp_image.channels == 4 # 4 regardless of the alpha argument above.
|
||||
|
||||
# Also slow and eats all your memory.
|
||||
tmp_image.pixels = array.flatten().tolist()
|
||||
tmp_image.pixels.foreach_set(pixels)
|
||||
|
||||
return _encode_temp_image(tmp_image, self.file_format)
|
||||
|
||||
|
@ -296,8 +204,13 @@ class ExportImage:
|
|||
try:
|
||||
tmp_image = image.copy()
|
||||
tmp_image.update()
|
||||
|
||||
if image.is_dirty:
|
||||
tmp_image.pixels = image.pixels[:]
|
||||
# Copy the pixels to get the changes
|
||||
tmp_buf = np.empty(image.size[0] * image.size[1] * 4, np.float32)
|
||||
image.pixels.foreach_get(tmp_buf)
|
||||
tmp_image.pixels.foreach_set(tmp_buf)
|
||||
tmp_buf = None # GC this
|
||||
|
||||
return _encode_temp_image(tmp_image, self.file_format)
|
||||
finally:
|
||||
|
@ -316,36 +229,3 @@ def _encode_temp_image(tmp_image: bpy.types.Image, file_format: str) -> bytes:
|
|||
|
||||
with open(tmpfilename, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _render_temp_scene(
|
||||
tmp_scene: bpy.types.Scene,
|
||||
width: int,
|
||||
height: int,
|
||||
file_format: str,
|
||||
color_mode: str,
|
||||
colorspace: str,
|
||||
) -> bytes:
|
||||
"""Set render settings, render to a file, and read back."""
|
||||
tmp_scene.render.resolution_x = width
|
||||
tmp_scene.render.resolution_y = height
|
||||
tmp_scene.render.resolution_percentage = 100
|
||||
tmp_scene.display_settings.display_device = colorspace
|
||||
tmp_scene.render.image_settings.color_mode = color_mode
|
||||
tmp_scene.render.dither_intensity = 0.0
|
||||
|
||||
# Turn off all metadata (stuff like use_stamp_date, etc.)
|
||||
for attr in dir(tmp_scene.render):
|
||||
if attr.startswith('use_stamp_'):
|
||||
setattr(tmp_scene.render, attr, False)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
tmpfilename = tmpdirname + "/img"
|
||||
tmp_scene.render.filepath = tmpfilename
|
||||
tmp_scene.render.use_file_extension = False
|
||||
tmp_scene.render.image_settings.file_format = file_format
|
||||
|
||||
bpy.ops.render.render(scene=tmp_scene.name, write_still=True)
|
||||
|
||||
with open(tmpfilename, "rb") as f:
|
||||
return f.read()
|
||||
|
|
Loading…
Reference in New Issue