Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export: KHR_materials_specular and KHR_materials_ior #1158

Closed
wants to merge 9 commits into from
67 changes: 44 additions & 23 deletions addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from io_scene_gltf2.io.exp import gltf2_io_binary_data
from io_scene_gltf2.io.exp import gltf2_io_image_data
from io_scene_gltf2.io.com import gltf2_io_debug
from io_scene_gltf2.blender.exp.gltf2_blender_image import Channel, ExportImage, FillImage
from io_scene_gltf2.blender.exp.gltf2_blender_image import Channel, SpecularColorSource, ExportImage, FillImage
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions

Expand Down Expand Up @@ -187,36 +187,37 @@ def __get_image_data(sockets, export_settings) -> ExportImage:
# in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
# resources.
results = [__get_tex_from_socket(socket, export_settings) for socket in sockets]

is_specular_color = False
for socket in sockets:
if socket.name == 'Specular' or socket.name == 'Specular Tint':
is_specular_color = True

composed_image = ExportImage()
for result, socket in zip(results, sockets):
if result.shader_node.image.channels == 0:
gltf2_io_debug.print_console("WARNING",
"Image '{}' has no color channels and cannot be exported.".format(
result.shader_node.image))
continue

# Assume that user know what he does, and that channels/images are already combined correctly for pbr
# If not, we are going to keep only the first texture found
# Example : If user set up 2 or 3 different textures for Metallic / Roughness / Occlusion
# Only 1 will be used at export
# This Warning is displayed in UI of this option
if export_settings['gltf_keep_original_textures']:
composed_image = ExportImage.from_original(result.shader_node.image)

else:
else:
# rudimentarily try follow the node tree to find the correct image data.
src_chan = Channel.R
for elem in result.path:
if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB):
src_chan = {
'R': Channel.R,
'G': Channel.G,
'B': Channel.B,
}[elem.from_socket.name]
if elem.from_socket.name == 'Alpha':
src_chan = Channel.A
if result:
src_chan = Channel.R
for elem in result.path:
if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB):
src_chan = {
'R': Channel.R,
'G': Channel.G,
'B': Channel.B,
}[elem.from_socket.name]
if elem.from_socket.name == 'Alpha':
src_chan = Channel.A

dst_chan = None
specular_color_src_type = None

# some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes)
if socket.name == 'Metallic':
Expand All @@ -225,13 +226,29 @@ def __get_image_data(sockets, export_settings) -> ExportImage:
dst_chan = Channel.G
elif socket.name == 'Occlusion':
dst_chan = Channel.R
elif socket.name == 'Alpha':
elif socket.name == 'Alpha' and len(sockets) > 1 and sockets[1] is not None:
dst_chan = Channel.A
elif socket.name == 'Clearcoat':
dst_chan = Channel.R
elif socket.name == 'Clearcoat Roughness':
dst_chan = Channel.G

elif is_specular_color and socket.name == 'Specular':
specular_color_src_type = SpecularColorSource.Specular
elif is_specular_color and socket.name == 'Specular Tint':
specular_color_src_type = SpecularColorSource.SpecularTint
elif is_specular_color and socket.name == 'Base Color':
specular_color_src_type = SpecularColorSource.BaseColor
elif is_specular_color and socket.name == 'Transmission':
specular_color_src_type = SpecularColorSource.Transmission
elif is_specular_color and socket.name == 'IOR':
specular_color_src_type = SpecularColorSource.IOR

if specular_color_src_type is None and result.shader_node.image.channels == 0:
gltf2_io_debug.print_console("WARNING",
"Image '{}' has no color channels and cannot be exported.".format(
result.shader_node.image))
continue

if dst_chan is not None:
composed_image.fill_image(result.shader_node.image, dst_chan, src_chan)

Expand All @@ -242,8 +259,12 @@ def __get_image_data(sockets, export_settings) -> ExportImage:
elif socket.name == 'Roughness' and not composed_image.is_filled(Channel.B):
composed_image.fill_white(Channel.B)
else:
# copy full image...eventually following sockets might overwrite things
composed_image = ExportImage.from_blender_image(result.shader_node.image)
if specular_color_src_type is not None:
# bake specular color texture
composed_image.fill_specular_color(result.shader_node.image if result else None, specular_color_src_type, socket.default_value)
else:
# copy full image...eventually following sockets might overwrite things
composed_image = ExportImage.from_blender_image(result.shader_node.image)

return composed_image

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,20 @@ def __gather_extensions(blender_material, export_settings):
if clearcoat_extension:
extensions["KHR_materials_clearcoat"] = clearcoat_extension

# TODO KHR_materials_pbrSpecularGlossiness
# KHR_materials_transmission

transmission_extension = __gather_transmission_extension(blender_material, export_settings)
if transmission_extension:
extensions["KHR_materials_transmission"] = transmission_extension

# KHR_materials_specular and KHR_materials_ior
ior_extension, specular_extension = __gather_ior_and_specular_extensions(blender_material, export_settings)
if ior_extension:
extensions["KHR_materials_ior"] = ior_extension
if specular_extension:
extensions["KHR_materials_specular"] = specular_extension

return extensions if extensions else None


Expand Down Expand Up @@ -386,3 +394,74 @@ def __gather_material_unlit(blender_material, export_settings):
export_user_extensions('gather_material_unlit_hook', export_settings, material, blender_material)

return material

def __gather_ior_and_specular_extensions(blender_material, export_settings):
lerp = lambda a, b, v: (1-v)*a + v*b
luminance = lambda c: 0.3 * c[0] + 0.6 * c[1] + 0.1 * c[2]

specular_ext_enabled = False
ior_ext_enabled = False

specular_extension = {}
ior_extension = {}

specular_socket = gltf2_blender_get.get_socket(blender_material, 'Specular')
specular_tint_socket = gltf2_blender_get.get_socket(blender_material, 'Specular Tint')
base_color_socket = gltf2_blender_get.get_socket(blender_material, 'Base Color')
transmission_socket = gltf2_blender_get.get_socket(blender_material, 'Transmission')
ior_socket = gltf2_blender_get.get_socket(blender_material, 'IOR')

specular_not_linked = isinstance(specular_socket, bpy.types.NodeSocket) and not specular_socket.is_linked
specular_tint_not_linked = isinstance(specular_tint_socket, bpy.types.NodeSocket) and not specular_tint_socket.is_linked
base_color_not_linked = isinstance(base_color_socket, bpy.types.NodeSocket) and not base_color_socket.is_linked
transmission_not_linked = isinstance(transmission_socket, bpy.types.NodeSocket) and not transmission_socket.is_linked
ior_not_linked = isinstance(ior_socket, bpy.types.NodeSocket) and not ior_socket.is_linked

specular = specular_socket.default_value if specular_not_linked else None
specular_tint = specular_tint_socket.default_value if specular_tint_not_linked else None
transmission = transmission_socket.default_value if transmission_not_linked else None
ior = ior_socket.default_value if ior_not_linked else 1.5 # textures not supported

no_texture = (specular_not_linked and specular_tint_not_linked and
(specular_tint == 0.0 or (specular_tint != 0.0 and base_color_not_linked)))

if ior != 1.5:
ior_ext_enabled = True
ior_extension['ior'] = ior

if no_texture:
if specular != 0.5 or specular_tint != 0.0:
specular_ext_enabled = True
base_color = base_color_socket.default_value[0:3] if base_color_not_linked else [0, 0, 0]
normalized_base_color = [bc / luminance(base_color) if luminance(base_color) > 0 else 0 for bc in base_color]
Copy link
Contributor

@scurest scurest Aug 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base_color can be None here (but only if specular_tint==0, so its value doesn't matter).

specular_color = [lerp(1, bc, specular_tint) for bc in normalized_base_color]

# The IOR dictates the maximal reflection strength, therefore we need to clamp
# reflection strenth of non-transmissive (aka plastic) fraction (if any)
plastic = [1/((ior - 1) / (ior + 1))**2 * 0.08 * specular * sc for sc in specular_color]
glass = specular_color
specular_extension['specularColorFactor'] = [lerp(plastic[c], glass[c], transmission) for c in range(0,3)]
else:
sockets = (specular_socket, specular_tint_socket, base_color_socket, transmission_socket, ior_socket)
primary_socket = specular_socket
if specular_not_linked:
primary_socket = specular_tint_socket
if specular_tint_not_linked:
primary_socket = base_color_socket
if base_color_not_linked:
primary_socket = transmission_socket
info = gltf2_blender_gather_texture_info.gather_texture_info(primary_socket, sockets, export_settings)
if info is None:
return None, None

specular_ext_enabled = True
specular_extension['specularColorTexture'] = info

# If specular>0.5, specular color may be >1. As we cannot store values >1 in a byte texture,
# we use the specular color factor to rescale the value range.
specular_extension['specularColorFactor'] = [2, 2, 2]

ior_extension = Extension('KHR_materials_ior', ior_extension, False) if ior_ext_enabled else None
specular_extension = Extension('KHR_materials_specular', specular_extension, False) if specular_ext_enabled else None

return ior_extension, specular_extension
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ def __gather_name(blender_shader_sockets, export_settings):


def __gather_sampler(blender_shader_sockets, export_settings):
shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets]
shader_nodes = [__get_tex_from_socket(socket) for socket in blender_shader_sockets]
if len(shader_nodes) > 1:
gltf2_io_debug.print_console("WARNING",
"More than one shader node tex image used for a texture. "
"The resulting glTF sampler will behave like the first shader node tex image.")
first_valid_shader_node = next(filter(lambda x: x is not None, shader_nodes)).shader_node
return gltf2_blender_gather_sampler.gather_sampler(
shader_nodes[0],
first_valid_shader_node,
export_settings)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,18 @@ def __gather_texture_info_helper(
blender_shader_sockets: typing.Tuple[bpy.types.NodeSocket],
kind: str,
export_settings):
if not __filter_texture_info(primary_socket, blender_shader_sockets, export_settings):
is_specular_color = False
for socket in blender_shader_sockets:
if socket and (socket.name == 'Specular' or socket.name == 'Specular Tint'):
is_specular_color = True

if not is_specular_color and not __filter_texture_info(primary_socket, blender_shader_sockets, export_settings):
return None

tex_transform, tex_coord = __gather_texture_transform_and_tex_coord(primary_socket, export_settings)

fields = {
'extensions': __gather_extensions(tex_transform, export_settings),
'extensions': None if is_specular_color else __gather_extensions(tex_transform, export_settings),
'extras': __gather_extras(blender_shader_sockets, export_settings),
'index': __gather_index(blender_shader_sockets, export_settings),
'tex_coord': tex_coord,
Expand Down
99 changes: 99 additions & 0 deletions addons/io_scene_gltf2/blender/exp/gltf2_blender_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import numpy as np
import tempfile
import enum
from io_scene_gltf2.io.com.gltf2_io_debug import print_console


class Channel(enum.IntEnum):
Expand All @@ -26,6 +27,13 @@ class Channel(enum.IntEnum):
B = 2
A = 3

class SpecularColorSource(enum.IntEnum):
Specular = 0
SpecularTint = 1
BaseColor = 2
Transmission = 3
IOR = 4

# These describe how an ExportImage's channels should be filled.

class FillImage:
Expand All @@ -38,6 +46,60 @@ class FillWhite:
"""Fills a channel with all ones (1.0)."""
pass

class FillSpecularColor:
"""Fills a channel of the specular color from a Blender image."""
def __init__(self):
self.size = None
self.images = {}
self.src_default_values = {} # used if image is None

def append(self, image: bpy.types.Image, src_type: SpecularColorSource, src_default_value):
self.images[src_type] = image
self.src_default_values[src_type] = src_default_value

if src_type is SpecularColorSource.IOR and image is not None:
# IOR textures not supported
self.images[SpecularColorSource.IOR] = None
self.src_default_values = 1.0

if image is not None:
if self.size is None:
# first image determines size
self.size = image.size
else:
# all other images must have the same size
if self.size[0] != image.size[0] or self.size[1] != image.size[1]:
print_console("WARNING",
"Specular, specular tint, transmission and/or base color textures have different "
"sizes. Textures will be ignored.")
self.images[src_type] = None

def has_image(self, src_type: SpecularColorSource):
return src_type in self.images and self.images[src_type] is not None

def image_size(self):
return (self.size[0], self.size[1])

def as_buffer(self, src_type: SpecularColorSource):
width, height = self.image_size()
if self.has_image(src_type):
out_buf = np.ones(height * width * 4, np.float32)
self.images[src_type].pixels.foreach_get(out_buf)
out_buf = np.reshape(out_buf, (height, width, 4))
if src_type == SpecularColorSource.BaseColor:
out_buf = out_buf[:,:,0:3]
else:
out_buf = out_buf[:,:,0]
else:
if src_type == SpecularColorSource.BaseColor:
rgb = [self.src_default_values[src_type][0],
self.src_default_values[src_type][1],
self.src_default_values[src_type][2]]
out_buf = np.full((height, width, 3), rgb)
else:
f = self.src_default_values[src_type]
out_buf = np.full((height, width, 1), f)
return out_buf

class ExportImage:
"""Custom image class.
Expand Down Expand Up @@ -66,6 +128,7 @@ class ExportImage:

def __init__(self, original=None):
self.fills = {}
self.specular_color_fill = None

# In case of keeping original texture images
self.original = original
Expand All @@ -87,6 +150,12 @@ def fill_image(self, image: bpy.types.Image, dst_chan: Channel, src_chan: Channe
def fill_white(self, dst_chan: Channel):
self.fills[dst_chan] = FillWhite()

def fill_specular_color(self, image: bpy.types.Image, src_type: SpecularColorSource, src_default_value):
self.fills[Channel.A] = FillWhite()
if self.specular_color_fill is None:
self.specular_color_fill = FillSpecularColor()
self.specular_color_fill.append(image, src_type, src_default_value)

def is_filled(self, chan: Channel) -> bool:
return chan in self.fills

Expand Down Expand Up @@ -120,6 +189,10 @@ def encode(self, mime_type: Optional[str]) -> bytes:
"image/png": "PNG"
}.get(mime_type, "PNG")

# Specular color path = we need to bake the specular color image
if self.specular_color_fill is not None:
return self.__encode_specular_color()

# Happy path = we can just use an existing Blender image
if self.__on_happy_path():
return self.__encode_happy()
Expand Down Expand Up @@ -213,6 +286,32 @@ def __encode_from_image(self, image: bpy.types.Image) -> bytes:
tmp_image = guard.image
return _encode_temp_image(tmp_image, self.file_format)

def __encode_specular_color(self) -> bytes:
lerp = lambda a, b, v: (1-v)*a + v*b
luminance = lambda c: 0.3 * c[:,:,0] + 0.6 * c[:,:,1] + 0.1 * c[:,:,2]
stack3 = lambda v: np.dstack([v]*3)

ior = self.specular_color_fill.src_default_values[SpecularColorSource.IOR]

specular_buf = self.specular_color_fill.as_buffer(SpecularColorSource.Specular)
specular_tint_buf = self.specular_color_fill.as_buffer(SpecularColorSource.SpecularTint)
base_color_buf = self.specular_color_fill.as_buffer(SpecularColorSource.BaseColor)
transmission_buf = self.specular_color_fill.as_buffer(SpecularColorSource.Transmission)

width, height = self.specular_color_fill.image_size()

normalized_base_color_buf = base_color_buf / stack3(luminance(base_color_buf))
np.nan_to_num(normalized_base_color_buf, copy=False, nan=0.0) # if luminance in a pixel was zero

sc = lerp(1, normalized_base_color_buf, stack3(specular_tint_buf))
plastic = 1/((ior - 1) / (ior + 1))**2 * 0.08 * stack3(specular_buf) * sc
glass = sc
out_buf = lerp(plastic, glass, stack3(transmission_buf))

out_buf = np.dstack((out_buf, np.ones((height, width))))
out_buf = np.reshape(out_buf, (width * height * 4))
out_buf *= 0.5 # specularColorFactor is 2
return self.__encode_from_numpy_array(out_buf.astype(np.float32), (width, height))

def _encode_temp_image(tmp_image: bpy.types.Image, file_format: str) -> bytes:
with tempfile.TemporaryDirectory() as tmpdirname:
Expand Down