From c4080626dff9094d7cf60b186ddc36a5fe51fb36 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 12 Dec 2023 17:32:49 +0500 Subject: [PATCH] style.add_surface_texture to work without blender material provided --- .../blenderbim/bim/module/style/operator.py | 22 +++-- .../api/style/add_surface_textures.py | 40 ++++++++-- .../api/style/test_add_surface_textures.py | 80 +++++++++++++++++++ 3 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/style/test_add_surface_textures.py diff --git a/src/blenderbim/blenderbim/bim/module/style/operator.py b/src/blenderbim/blenderbim/bim/module/style/operator.py index 3e647ba718..5feef14ee6 100644 --- a/src/blenderbim/blenderbim/bim/module/style/operator.py +++ b/src/blenderbim/blenderbim/bim/module/style/operator.py @@ -566,7 +566,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): "Define shading/render style first", ) return {"CANCELLED"} - textures = tool.Ifc.run("style.add_surface_textures", material=material, uv_maps=[]) + textures = tool.Ifc.run("style.add_surface_textures", textures=self.get_texture_attributes(), uv_maps=[]) texture_style = tool.Ifc.run( "style.add_surface_style", style=self.style, @@ -603,9 +603,6 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): ) tool.Loader.create_surface_style_rendering(material, surface_style) elif self.props.is_editing_class == "IfcSurfaceStyleWithTextures": - # TODO: rework add_surface_textures to work without blender - # otherwise we lose textures that are not used in the shader - # and we also doesn't recognize relative paths if .blend file is not saved # TODO: provide `uv_maps` - need to rework .get_uv_maps not to depend on a single representation material = tool.Ifc.get_object(self.style) shading_style = self.rendering_style or self.shading_style @@ -617,7 +614,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): "Define shading/render style first", ) return {"CANCELLED"} - textures = tool.Ifc.run("style.add_surface_textures", material=material, uv_maps=[]) + textures = tool.Ifc.run("style.add_surface_textures", textures=self.get_texture_attributes(), uv_maps=[]) if textures: texture_style = tool.Ifc.run( "style.add_surface_style", @@ -670,6 +667,21 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): "SpecularHighlight": specular_highlight, } + # TODO: support RepeatS/RepeatT params in UI: + # add it to prop.Texture and Style.get_texture_style_data_from_props + def get_texture_attributes(self): + textures = [] + for texture in self.props.textures: + texture_data = { + "URLReference": texture.path, + "Mode": texture.mode, + "RepeatS": True, + "RepeatT": True, + "uv_mode": self.props.uv_mode, + } + textures.append(texture_data) + return textures + def color_to_dict(self, x): return {"Red": x[0], "Green": x[1], "Blue": x[2]} diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py index e11650e2bd..70a0da1e45 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py @@ -21,10 +21,8 @@ import ifcopenshell.api class Usecase: - def __init__(self, file, material=None, uv_maps=None): - """Add a surface texture based on a Blender material definition - - Warning: this API can only be used with Blender data structures. + def __init__(self, file, material=None, uv_maps=None, textures=None): + """Add surface texture based on a Blender material definition or texture data. :param material: The Blender material definition with a node tree that is compatible with glTF. See one of the valid combinations here: @@ -34,12 +32,26 @@ class Usecase: IfcTessellatedFaceSets that the representation has, obtained from the HasTextures attribute. :type uv_maps: list[ifcopenshell.entity_instance.entity_instance] + :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) + :type textures: list[dict] :return: A list of IfcImageTexture :rtype: list[ifcopenshell.entity_instance.entity_instance] """ # TODO: This usecase currently depends on Blender's data model self.file = file - self.settings = {"material": material, "uv_maps": uv_maps or []} + self.settings = {"material": material, "uv_maps": uv_maps or [], "textures": textures or []} def execute(self): if self.file.schema == "IFC2X3": @@ -51,6 +63,23 @@ class Usecase: # 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: @@ -73,6 +102,7 @@ class Usecase: 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): diff --git a/src/ifcopenshell-python/test/api/style/test_add_surface_textures.py b/src/ifcopenshell-python/test/api/style/test_add_surface_textures.py new file mode 100644 index 0000000000..bdca230d99 --- /dev/null +++ b/src/ifcopenshell-python/test/api/style/test_add_surface_textures.py @@ -0,0 +1,80 @@ +# 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 . + +import pytest +import test.bootstrap +import ifcopenshell.api + + +class TestAddSurfaceTexture(test.bootstrap.IFC4): + def get_default_texture_data(self): + return [ + {"Mode": "DIFFUSE", "RepeatS": True, "RepeatT": True, "URLReference": "diffuse.jpg"}, + {"Mode": "NORMAL", "RepeatS": False, "RepeatT": False, "URLReference": "normal.jpg"}, + {"Mode": "METALLICROUGHNESS", "RepeatS": True, "RepeatT": True, "URLReference": "metallic_roughness.jpg"}, + {"Mode": "OCCLUSION", "RepeatS": True, "RepeatT": True, "URLReference": "ambient_occlusion.jpg"}, + ] + + def compare_texture_to_data(self, texture, data, uv_maps=[]): + texture_data = texture.get_info() + for attribute in ("Mode", "RepeatS", "RepeatT", "URLReference"): + assert texture_data[attribute] == data.get(attribute, None) + + uv_mode = data.get("uv_mode", None) + if uv_mode is None: + assert texture.IsMappedBy == () + elif uv_mode == "Generated": + assert len(texture.IsMappedBy) == 1 + assert texture.IsMappedBy[0].Mode == "COORD" + elif uv_mode == "Camera": + assert len(texture.IsMappedBy) == 1 + assert texture.IsMappedBy[0].Mode == "COORD-EYE" + elif uv_mode == "UV": + assert set(texture.IsMappedBy) == set(uv_maps) + + def test_add_surface_textures_from_data(self): + texture_data = self.get_default_texture_data() + + textures = ifcopenshell.api.run("style.add_surface_textures", self.file, textures=texture_data) + assert len(list(self.file)) == len(texture_data) + + for texture, data in zip(textures, texture_data): + self.compare_texture_to_data(texture, data) + + def test_add_surface_textures_from_data_with_uv_mode(self): + texture_data = self.get_default_texture_data() + texture_data[0]["uv_mode"] = "Generated" + texture_data[1]["uv_mode"] = "Camera" + texture_data[2]["uv_mode"] = "UV" + texture_data[3]["uv_mode"] = None + + textures = ifcopenshell.api.run("style.add_surface_textures", self.file, textures=texture_data) + for texture, data in zip(textures, texture_data): + self.compare_texture_to_data(texture, data) + + def test_add_surface_textures_from_data_with_uv_maps(self): + texture_data = self.get_default_texture_data() + texture_data[0]["uv_mode"] = "Generated" + texture_data[1]["uv_mode"] = "Camera" + texture_data[2]["uv_mode"] = "UV" + texture_data[3]["uv_mode"] = None + + uv_maps = [self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[], Mode="COORD") for i in range(5)] + textures = ifcopenshell.api.run("style.add_surface_textures", self.file, textures=texture_data, uv_maps=uv_maps) + for texture, data in zip(textures, texture_data): + self.compare_texture_to_data(texture, data, uv_maps)