# IfcOpenShell - IFC toolkit and geometry engine # Copyright (C) 2022 Dion Moult # # This file is part of IfcOpenShell. # # IfcOpenShell is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # IfcOpenShell is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . from __future__ import annotations from typing import TYPE_CHECKING, Any, Optional import ifcopenshell if TYPE_CHECKING: import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import] def add_surface_textures( file: ifcopenshell.file, material: Optional[bpy.types.Material] = None, textures: Optional[list[dict]] = None, uv_maps: Optional[list[ifcopenshell.entity_instance]] = None, ) -> list[ifcopenshell.entity_instance]: """Add surface texture based on a Blender material definition or texture data. Either `material` or `textures` should be provided. :param material: The Blender material definition with a node tree that is compatible with glTF. See one of the valid combinations here: https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html :param uv_maps: A list of IfcIndexedTextureMap for any IfcTessellatedFaceSets that the representation has, obtained from the HasTextures attribute. :param textures: A list of dictionaries containing: 1. Attributes to create IfcImageTexture. 2. One additional parameter `uv_mode` to map IfcImageTexture to correct IfcTextureCoordinate type. Possible `uv_mode` values: * `UV` - use IfcTextureCoordinate from `uv_maps` parameter; * `Generated` - IfcTextureCoordinateGenerator with mode COORD (autogenerated UV based on geometry); * `Camera` - IfcTextureCoordinateGenerator with mode COORD_EYE (autogenerated UV based on camera position) :return: A list of IfcImageTexture """ usecase = Usecase() # TODO: This usecase currently depends on Blender's data model usecase.file = file usecase.settings = {"material": material, "uv_maps": uv_maps or [], "textures": textures or []} return usecase.execute() class Usecase: file: ifcopenshell.file settings: dict[str, Any] def execute(self): if self.file.schema == "IFC2X3": # TODO: research how compatible IFC2X3 and IFC4 textures are return [] # We optimistically assume the user has specified one of these valid combinations # https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html # glTF, X3D, and IFC are compatible. As long as they have something that # loosely resembles the node tree, we treat it as valid. self.textures = [] for texture in self.settings["textures"]: uv_mode = texture.get("uv_mode", None) texture_data = texture.copy() texture_data.pop("uv_mode", None) texture = self.file.create_entity("IfcImageTexture", **texture_data) if uv_mode == "Generated": self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD") elif uv_mode == "Camera": self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD-EYE") elif uv_mode == "UV": self.apply_uv_map_to_texture(texture) self.textures.append(texture) if self.settings["material"] is None: return self.textures output = {n.type: n for n in self.settings["material"].node_tree.nodes}.get("OUTPUT_MATERIAL", None) if not output: return self.textures bsdf = output.inputs["Surface"].links[0].from_node if bsdf.type == "ADD_SHADER": for socket in bsdf.inputs: if socket.links and socket.links[0].from_node.type == "BSDF_PRINCIPLED": bsdf = socket.links[0].from_node break if bsdf.type == "MIX_SHADER": self.detect_unlit_emissive_map(bsdf) elif bsdf.type == "BSDF_PRINCIPLED": self.detect_normal_map(bsdf) self.detect_emissive_map(bsdf) self.detect_metallicroughness_map(bsdf) self.detect_occlusion_map() self.detect_diffuse_map(bsdf) # We do not support Phong shading. What year is this, 1995? return self.textures def detect_unlit_emissive_map(self, bsdf): for socket in bsdf.inputs: if socket.links and socket.links[0].from_node.type == "TEX_IMAGE": return self.create_surface_texture(socket.links[0].from_node, "EMISSIVE") def detect_normal_map(self, bsdf): if bsdf.inputs["Normal"].links and bsdf.inputs["Normal"].links[0].from_node.type == "NORMAL_MAP": normal = bsdf.inputs["Normal"].links[0].from_node if normal.inputs["Color"].links and normal.inputs["Color"].links[0].from_node.type == "TEX_IMAGE": return self.create_surface_texture(normal.inputs["Color"].links[0].from_node, "NORMAL") def detect_emissive_map(self, bsdf): if bsdf.outputs[0].links[0].to_node.type != "ADD_SHADER": return bsdf = bsdf.outputs[0].links[0].to_node for socket in bsdf.inputs: if socket.links and socket.links[0].from_node.type == "EMISSION": bsdf = socket.links[0].from_node if bsdf.inputs["Color"].links and bsdf.inputs["Color"].links[0].from_node.type == "TEX_IMAGE": return self.create_surface_texture(bsdf.inputs["Color"].links[0].from_node, "EMISSIVE") def detect_metallicroughness_map(self, bsdf): if bsdf.inputs["Metallic"].links and bsdf.inputs["Metallic"].links[0].from_node.type == "SEPRGB": seprgb = bsdf.inputs["Metallic"].links[0].from_node if seprgb.inputs["Image"].links and seprgb.inputs["Image"].links[0].from_node.type == "TEX_IMAGE": return self.create_surface_texture(seprgb.inputs["Image"].links[0].from_node, "METALLICROUGHNESS") if bsdf.inputs["Roughness"].links and bsdf.inputs["Roughness"].links[0].from_node.type == "SEPRGB": seprgb = bsdf.inputs["Roughness"].links[0].from_node if seprgb.inputs["Image"].links and seprgb.inputs["Image"].links[0].from_node.type == "TEX_IMAGE": return self.create_surface_texture(seprgb.inputs["Image"].links[0].from_node, "METALLICROUGHNESS") def detect_occlusion_map(self): for node in self.settings["material"].node_tree.nodes: if ( node.type != "GROUP" or not node.node_tree or node.node_tree.name != "glTF Material Output" or not node.inputs or not node.inputs[0].links ): continue from_node = node.inputs[0].links[0].from_node if from_node.type == "SEPRGB": sep = from_node if sep.inputs["Image"].links and sep.inputs["Image"].links[0].from_node.type == "TEX_IMAGE": return self.create_surface_texture(sep.inputs["Image"].links[0].from_node, "OCCLUSION") elif from_node.type == "TEX_IMAGE": return self.create_surface_texture(from_node, "OCCLUSION") def detect_diffuse_map(self, bsdf): links = bsdf.inputs["Base Color"].links if links and links[0].from_node.type == "TEX_IMAGE": return self.create_surface_texture(links[0].from_node, "DIFFUSE") def create_surface_texture(self, node, mode): import bonsai.tool as tool texture = self.file.create_entity( "IfcImageTexture", RepeatS=node.extension == "REPEAT", RepeatT=node.extension == "REPEAT", Mode=mode, URLReference=tool.Blender.blender_path_to_posix(node.image.filepath), ) self.textures.append(texture) self.process_texture_coordinates(node, texture) def process_texture_coordinates(self, node, texture): if node.inputs["Vector"].links and node.inputs["Vector"].links[0].from_node.type == "TEX_COORD": if node.inputs["Vector"].links[0].from_socket.name == "UV": self.apply_uv_map_to_texture(texture) elif node.inputs["Vector"].links[0].from_socket.name == "Generated": self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD") elif node.inputs["Vector"].links[0].from_socket.name == "Camera": self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD-EYE") elif node.inputs["Vector"].links and node.inputs["Vector"].links[0].from_node.type == "UVMAP": self.apply_uv_map_to_texture(texture) def apply_uv_map_to_texture(self, texture): for uv_map in self.settings["uv_maps"]: maps = set(uv_map.Maps or []) maps.add(texture) uv_map.Maps = list(maps)