glTF exporter: perf: use foreach_{get,set} for image encoding

We can now remove compositor code, no more needed
This commit is contained in:
Julien Duroure 2020-04-11 15:41:05 +02:00
parent 8dd0687a67
commit dfadb306b0
3 changed files with 24 additions and 146 deletions

View File

@ -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': '',

View File

@ -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

View File

@ -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()