Export ImageTexture node for an image identified in material node tree #39
@ -7,6 +7,9 @@
|
||||
# - Doesn't handle multiple UV textures on a single mesh (create a mesh for each texture).
|
||||
# - Can't get the texture array associated with material * not the UV ones.
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("export_x3d")
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
@ -185,6 +188,154 @@ def h3d_is_object_view(scene, obj):
|
||||
return False
|
||||
|
||||
|
||||
# node based material identifiers
|
||||
BSDF_PRINCIPLED = 'BSDF_PRINCIPLED'
|
||||
BASE_COLOR = 'Base Color'
|
||||
IMAGE = 'image'
|
||||
|
||||
# values of bl_idname for shader nodes to be located
|
||||
# reference Python API list of subclasses of ShaderNode
|
||||
# at https://docs.blender.org/api/current/bpy.types.ShaderNode.html#bpy.types.ShaderNode
|
||||
MATERIAL_OUTPUT= "ShaderNodeOutputMaterial"
|
||||
BSDF_PRINCIPLED = "ShaderNodeBsdfPrincipled"
|
||||
IMAGE_TEXTURE = "ShaderNodeTexImage"
|
||||
|
||||
# supported values of Image Texture Node extension property
|
||||
REPEAT="REPEAT"
|
||||
CLIP ="CLIP"
|
||||
|
||||
class ImageInMaterial:
|
||||
"""
|
||||
An instance of this class is a callable object that takes as input arguemnt
|
||||
a Material object, and returns an Image instance or None.
|
||||
It is defined through a class so as to have a built-in cache mechanism
|
||||
"""
|
||||
class ImageExportRecord:
|
||||
"""
|
||||
a collection of properties of an image used as a texture, relevant
|
||||
for export of an X3D ImageTextureNode
|
||||
|
||||
reference:
|
||||
Blender ImageTexture: https://docs.blender.org/manual/en/latest/render/shader_nodes/textures/image.html
|
||||
|
||||
instance properties:
|
||||
image -- the Blender Image instance
|
||||
|
||||
extension -- a string enum value which, in the X3D context, governs
|
||||
whether the image should be repeated for UV coordinates
|
||||
outside the (0,0) to (1,1) square. Blender TextureImages
|
||||
have "extension" options not implemented by X3D
|
||||
|
||||
is_packed -- a boolean value, if true then the image data is to be
|
||||
saved to a disk file associated with the output X3D file.
|
||||
The name packed comes from the initial method of determining
|
||||
whether this file needs to be exported by the presence of
|
||||
a Blender PackedFile instance connected to the Blender image
|
||||
|
||||
url -- if packed, then this string value is to be the URL
|
||||
included in the ImageTexture node and also used to
|
||||
define where the image should be saved (relative to the
|
||||
location of the X3D file.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.image = None
|
||||
self.extension = None
|
||||
self.url = None
|
||||
|
||||
@property
|
||||
def is_packed(self):
|
||||
return self.url is not None
|
||||
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
cachedResult will be a dictionary whose keys are Material instances
|
||||
and values are instance of
|
||||
"""
|
||||
self.cachedResult = dict()
|
||||
|
||||
def __call__(self, material):
|
||||
if material not in self.cachedResult:
|
||||
self.cachedResult[material] = self.evaluate(material)
|
||||
return self.cachedResult[material]
|
||||
|
||||
@staticmethod
|
||||
def _find_node_by_idname_(nodes, idname):
|
||||
"""
|
||||
nodes a sequence of Nodes, idname a string
|
||||
if 0 targets are found, returns None
|
||||
if 1 target is found returns that Node
|
||||
if more than 1 targets are found prints warning string and returns first found
|
||||
|
||||
"""
|
||||
logger.debug("enter _find_node_by_idname_ search for %s in %r" % (idname, nodes))
|
||||
nodelist = [nd for nd in nodes if nd.bl_idname == idname]
|
||||
logger.debug("result _find_node_by_idname_ found %i" % len(nodelist))
|
||||
if len(nodelist) == 0:
|
||||
return None
|
||||
if len(nodelist) > 1:
|
||||
logger.warn("_find_node_by_idname_ : multiple (%i) nodes of type %s found" % (len(nodelist), idname))
|
||||
return nodelist[0]
|
||||
|
||||
def evaluate(self, material):
|
||||
logger.debug("evaluating image in material %s" % material.name)
|
||||
|
||||
material_output = self._find_node_by_idname_( material.node_tree.nodes, MATERIAL_OUTPUT)
|
||||
if material_output is None:
|
||||
logger.warn("%s not found in material %s" % (MATERIAL_OUTPUT, material.name))
|
||||
return None
|
||||
|
||||
bsdf_principled = self._find_node_by_idname_(
|
||||
[ndlink.from_node for ndlink in material_output.inputs.get("Surface").links],
|
||||
BSDF_PRINCIPLED)
|
||||
if bsdf_principled is None : return None
|
||||
|
||||
image_texture = self._find_node_by_idname_(
|
||||
[ndlink.from_node for ndlink in bsdf_principled.inputs.get(BASE_COLOR).links],
|
||||
IMAGE_TEXTURE )
|
||||
if image_texture is None: return None
|
||||
|
||||
logger.debug("located %s ShaderNode" % IMAGE_TEXTURE)
|
||||
|
||||
|
||||
x3d_supported_file_extension = {"PNG" : ".png","JPEG" : ".jpg"}
|
||||
if image_texture.image.file_format not in x3d_supported_file_extension:
|
||||
logger.warn("images of format %s not supported" % image_texture.image.file_format)
|
||||
return None
|
||||
|
||||
retVal = self.ImageExportRecord()
|
||||
retVal.image = image_texture.image
|
||||
|
||||
# ref https://docs.blender.org/api/current/bpy.types.ShaderNodeTexImage.html#bpy.types.ShaderNodeTexImage.extension
|
||||
x3d_supported_extension = [CLIP, REPEAT]
|
||||
if image_texture.extension in x3d_supported_extension:
|
||||
retVal.extension = image_texture.extension
|
||||
else:
|
||||
logger.warn("image_texture.extension value %s unsupported in X3D" % image_texture.extension)
|
||||
retVal.extension=REPEAT
|
||||
|
||||
packed_file = image_texture.image.packed_file
|
||||
if packed_file is not None and packed_file.size > 0:
|
||||
from os.path import splitext, basename
|
||||
name_base = splitext( basename(image_texture.image.name))[0]
|
||||
url_ext = x3d_supported_file_extension[image_texture.image.file_format]
|
||||
retVal.url = name_base +url_ext
|
||||
|
||||
logger.info("ImageExportRecord : %s packed: %r url: %s : extension: %r" % \
|
||||
(retVal.image.name, retVal.is_packed,
|
||||
retVal.url , retVal.extension))
|
||||
|
||||
return retVal
|
||||
|
||||
def imagesToSave(self):
|
||||
"""
|
||||
a generator that yields the ImageExportRecord instances
|
||||
for images that have packed attribute True
|
||||
"""
|
||||
for image_export_record in self.cachedResult.values():
|
||||
if image_export_record and ( image_export_record.is_packed or True ):
|
||||
yield image_export_record
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions for writing output file
|
||||
# -----------------------------------------------------------------------------
|
||||
@ -275,6 +426,7 @@ def export(file,
|
||||
gpu_shader_cache[None] = gpu.export_shader(scene, gpu_shader_dummy_mat)
|
||||
h3d_material_route = []
|
||||
|
||||
imageInMaterial = ImageInMaterial()
|
||||
# -------------------------------------------------------------------------
|
||||
# File Writing Functions
|
||||
# -------------------------------------------------------------------------
|
||||
@ -512,27 +664,6 @@ def export(file,
|
||||
|
||||
is_coords_written = False
|
||||
|
||||
mesh_materials = mesh.materials[:]
|
||||
if not mesh_materials:
|
||||
mesh_materials = [None]
|
||||
|
||||
mesh_material_tex = [None] * len(mesh_materials)
|
||||
mesh_material_mtex = [None] * len(mesh_materials)
|
||||
mesh_material_images = [None] * len(mesh_materials)
|
||||
|
||||
for i, material in enumerate(mesh_materials):
|
||||
if 0 and material:
|
||||
for mtex in material.texture_slots:
|
||||
if mtex:
|
||||
tex = mtex.texture
|
||||
if tex and tex.type == 'IMAGE':
|
||||
image = tex.image
|
||||
if image:
|
||||
mesh_material_tex[i] = tex
|
||||
mesh_material_mtex[i] = mtex
|
||||
mesh_material_images[i] = image
|
||||
break
|
||||
|
||||
# fast access!
|
||||
mesh_vertices = mesh.vertices[:]
|
||||
mesh_loops = mesh.loops[:]
|
||||
@ -540,21 +671,14 @@ def export(file,
|
||||
mesh_polygons_materials = [p.material_index for p in mesh_polygons]
|
||||
mesh_polygons_vertices = [p.vertices[:] for p in mesh_polygons]
|
||||
|
||||
if len(set(mesh_material_images)) > 0: # make sure there is at least one image
|
||||
mesh_polygons_image = [mesh_material_images[material_index] for material_index in mesh_polygons_materials]
|
||||
else:
|
||||
mesh_polygons_image = [None] * len(mesh_polygons)
|
||||
mesh_polygons_image_unique = set(mesh_polygons_image)
|
||||
|
||||
# group faces
|
||||
polygons_groups = {}
|
||||
for material_index in range(len(mesh_materials)):
|
||||
for image in mesh_polygons_image_unique:
|
||||
polygons_groups[material_index, image] = []
|
||||
del mesh_polygons_image_unique
|
||||
for material_index in range(len(mesh.materials)):
|
||||
polygons_groups[material_index] = []
|
||||
|
||||
for i, (material_index, image) in enumerate(zip(mesh_polygons_materials, mesh_polygons_image)):
|
||||
polygons_groups[material_index, image].append(i)
|
||||
|
||||
for i, material_index in enumerate(mesh_polygons_materials):
|
||||
polygons_groups[material_index].append(i)
|
||||
|
||||
# Py dict are sorted now, so we can use directly polygons_groups.items()
|
||||
# and still get consistent reproducible outputs.
|
||||
@ -587,9 +711,9 @@ def export(file,
|
||||
for ltri in mesh.loop_triangles:
|
||||
polygons_to_loop_triangles_indices[ltri.polygon_index].append(ltri)
|
||||
|
||||
for (material_index, image), polygons_group in polygons_groups.items():
|
||||
for material_index, polygons_group in polygons_groups.items():
|
||||
if polygons_group:
|
||||
material = mesh_materials[material_index]
|
||||
material = mesh.materials[material_index]
|
||||
|
||||
fw('%s<Shape>\n' % ident)
|
||||
ident += '\t'
|
||||
@ -627,33 +751,15 @@ def export(file,
|
||||
fw('%s<Appearance>\n' % ident)
|
||||
ident += '\t'
|
||||
|
||||
if image and not use_h3d:
|
||||
writeImageTexture(ident, image)
|
||||
|
||||
# transform by mtex
|
||||
loc = mesh_material_mtex[material_index].offset[:2]
|
||||
|
||||
# mtex_scale * tex_repeat
|
||||
sca_x, sca_y = mesh_material_mtex[material_index].scale[:2]
|
||||
|
||||
sca_x *= mesh_material_tex[material_index].repeat_x
|
||||
sca_y *= mesh_material_tex[material_index].repeat_y
|
||||
|
||||
# flip x/y is a sampling feature, convert to transform
|
||||
if mesh_material_tex[material_index].use_flip_axis:
|
||||
rot = math.pi / -2.0
|
||||
sca_x, sca_y = sca_y, -sca_x
|
||||
else:
|
||||
rot = 0.0
|
||||
|
||||
ident_step = ident + (' ' * (-len(ident) +
|
||||
fw('%s<TextureTransform ' % ident)))
|
||||
fw('\n')
|
||||
# fw('center="%.6f %.6f" ' % (0.0, 0.0))
|
||||
fw(ident_step + 'translation="%.6f %.6f"\n' % loc)
|
||||
fw(ident_step + 'scale="%.6f %.6f"\n' % (sca_x, sca_y))
|
||||
fw(ident_step + 'rotation="%.6f"\n' % rot)
|
||||
fw(ident_step + '/>\n')
|
||||
# this is a location at which we should
|
||||
# invoke
|
||||
# writeImageTexture(ident, image)
|
||||
# and also potentially write
|
||||
# the TextureTransform
|
||||
imageRecord = imageInMaterial(material)
|
||||
if imageRecord:
|
||||
logger.info("output ImageTexture for %s" % imageRecord.image.name )
|
||||
writeImageTexture(ident, imageRecord)
|
||||
|
||||
if use_h3d:
|
||||
mat_tmp = material if material else gpu_shader_dummy_mat
|
||||
@ -1237,8 +1343,12 @@ def export(file,
|
||||
|
||||
fw('%s</ComposedShader>\n' % ident)
|
||||
|
||||
def writeImageTexture(ident, image):
|
||||
def writeImageTexture(ident, image_record):
|
||||
image=image_record.image
|
||||
image_id = quoteattr(unique_name(image, IM_ + image.name, uuid_cache_image, clean_func=clean_def, sep="_"))
|
||||
logger.info("write ImageTexture X3D node for %s format %s" % (image.name, image.file_format ))
|
||||
|
||||
|
||||
|
||||
if image.tag:
|
||||
fw('%s<ImageTexture USE=%s />\n' % (ident, image_id))
|
||||
@ -1249,24 +1359,49 @@ def export(file,
|
||||
fw('%s<ImageTexture ' % ident)))
|
||||
fw('DEF=%s\n' % image_id)
|
||||
|
||||
# collect image paths, can load multiple
|
||||
# [relative, name-only, absolute]
|
||||
filepath = image.filepath
|
||||
filepath_full = bpy.path.abspath(filepath, library=image.library)
|
||||
filepath_ref = bpy_extras.io_utils.path_reference(filepath_full, base_src, base_dst, path_mode, "textures", copy_set, image.library)
|
||||
filepath_base = os.path.basename(filepath_full)
|
||||
# both branches of the following if-then block will
|
||||
# set a variable images to a sequence of strings, each string
|
||||
# a url to be entered into the MFString value of the
|
||||
# attribute url of the X3D ImageTexture element
|
||||
|
||||
images = [
|
||||
filepath_ref,
|
||||
filepath_base,
|
||||
]
|
||||
if path_mode != 'RELATIVE':
|
||||
images.append(filepath_full)
|
||||
if (not image_record.is_packed):
|
||||
# this is the legacy algorithm for determining
|
||||
# a set of urls based on value of image.filepath and the
|
||||
# argument path_mode passed in to the export function
|
||||
# the idea is that this image has been from a file
|
||||
# and that same file locationwill be used as the source of texture image
|
||||
# for the X3D exported file.
|
||||
logger.debug("image.filepath: %s" % image.filepath)
|
||||
filepath = image.filepath
|
||||
filepath_full = bpy.path.abspath(filepath, library=image.library)
|
||||
filepath_ref = bpy_extras.io_utils.path_reference(filepath_full, base_src, base_dst, path_mode, "textures", copy_set, image.library)
|
||||
filepath_base = os.path.basename(filepath_full)
|
||||
|
||||
images = [f.replace('\\', '/') for f in images]
|
||||
images = [
|
||||
filepath_ref,
|
||||
filepath_base,
|
||||
]
|
||||
if path_mode != 'RELATIVE':
|
||||
images.append(filepath_full)
|
||||
|
||||
images = [f.replace('\\', '/') for f in images]
|
||||
else:
|
||||
# this is the choice where image_record.is_packed is true
|
||||
# meaning that the image data can be stored in the blend data
|
||||
# This happens when the image is retrieved from the binary data
|
||||
# in a glTF/glb
|
||||
images = [ image_record.url ]
|
||||
|
||||
# note: the following has the effect of eliminating duplicate paths
|
||||
# while preserving order-priority
|
||||
images = [f for i, f in enumerate(images) if f not in images[:i]]
|
||||
|
||||
logger.info("node urls: %s" % (images,))
|
||||
fw(ident_step + "url='%s'\n" % ' '.join(['"%s"' % escape(f) for f in images]))
|
||||
|
||||
# default value of repeatS, repeatT fields is true, so only need to
|
||||
# specify if extension value is CLIP
|
||||
if image_record.extension == CLIP:
|
||||
fw(ident_step + "repeatS='false' repeatT='false'")
|
||||
fw(ident_step + '/>\n')
|
||||
|
||||
def writeBackground(ident, world):
|
||||
@ -1517,6 +1652,11 @@ def export(file,
|
||||
# print(copy_set)
|
||||
bpy_extras.io_utils.path_reference_copy(copy_set)
|
||||
|
||||
for svRecord in imageInMaterial.imagesToSave():
|
||||
image_filepath = os.path.join(base_dst, svRecord.url)
|
||||
logger.info("writing image for texture to %s" % image_filepath)
|
||||
svRecord.image.save( filepath = image_filepath )
|
||||
|
||||
print('Info: finished X3D export to %r' % file.name)
|
||||
|
||||
|
||||
@ -1559,6 +1699,7 @@ def save(context,
|
||||
name_decorations=True
|
||||
):
|
||||
|
||||
logger.info("save: context %r to filepath %r" % (context,filepath))
|
||||
bpy.path.ensure_ext(filepath, '.x3dz' if use_compress else '.x3d')
|
||||
|
||||
if bpy.ops.object.mode_set.poll():
|
||||
|
Loading…
Reference in New Issue
Block a user
why is this temporary folder needed? Could you add that to the comment?