205 lines
9.4 KiB
Python
205 lines
9.4 KiB
Python
# IfcOpenShell - IFC toolkit and geometry engine
|
|
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
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)
|