Export ImageTexture node for an image identified in material node tree #39

Open
Vincent Marchetti wants to merge 12 commits from vmarchetti/io_scene_x3d:image-export into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
Showing only changes of commit 052f53a7d4 - Show all commits

View File

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

why is this temporary folder needed? Could you add that to the comment?

why is this temporary folder needed? Could you add that to the comment?
"""
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():