mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Bonsai: always load texture UV maps so textured models render out of the box
Fixes the Bonsai side of #6702. All three official buildingSMART texture examples (tessellation with blob, image and pixel texture) imported with a correctly decoded image wired into the material, but rendered untextured because the mesh UV layer from IfcIndexedTriangleTextureMap was only applied when the Load Indexed Maps advanced setting was enabled, and it defaults to off (9dff1441d7disabled it for the cost of indexed colour maps, which took texture UVs down with it). Texture UV maps are now loaded unconditionally: the work is gated on the material actually having an IfcSurfaceStyleWithTextures, so untextured models pay nothing (verified: identical material fingerprints and load time within noise on a 1131 mesh model). The load_indexed_maps setting keeps gating indexed colour maps only, as intended by9dff1441d7, and its label now says so. AddReferenceImage no longer needs its Show Texture in Solid mode (slow) checkbox, whose only effect was flipping that project setting to force UV loading; reference images now always get their UVs. Verified against all four official tessellated-shape-with-style examples: blob/image/pixel now import with correct per-loop UVs (64/64 triangles exact in each) on a default project load; individual-colors still requires the colour map setting. A self-contained regression test locks the behaviour, and the supported texture cases are now documented. This commit was generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -84,7 +84,9 @@ class MaterialCreator:
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
self.parse_element_type_material_styles(element)
|
||||
self.parsed_meshes.add(self.mesh.name)
|
||||
if self.ifc_import_settings.load_indexed_maps:
|
||||
# Texture UV maps are always loaded: they only cost something when a
|
||||
# texture style is actually present and textures render wrong without them.
|
||||
# Indexed colour maps stay behind the load_indexed_maps setting.
|
||||
self.load_texture_maps(shape_has_openings)
|
||||
self.assign_material_slots_to_faces()
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
@@ -4035,11 +4035,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
precision=3,
|
||||
unit="LENGTH",
|
||||
)
|
||||
show_texture_solid_mode: bpy.props.BoolProperty(
|
||||
name="Show Texture in Solid mode (slow)",
|
||||
description="Show Texture in Solid mode (slow)",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -4084,13 +4079,10 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
layout.prop(self, "use_relative_path")
|
||||
else:
|
||||
self.use_relative_path = False
|
||||
layout.prop(self, "show_texture_solid_mode")
|
||||
layout.prop(self, "x_length")
|
||||
layout.prop(self, "y_length")
|
||||
|
||||
def _execute(self, context):
|
||||
project_props = tool.Project.get_project_props()
|
||||
project_props.load_indexed_maps = self.show_texture_solid_mode
|
||||
space = tool.Blender.get_view3d_space()
|
||||
if space.shading.color_type != "TEXTURE":
|
||||
space.shading.color_type = "TEXTURE"
|
||||
|
||||
@@ -431,8 +431,8 @@ class BIMProjectProperties(PropertyGroup):
|
||||
element_offset: IntProperty(name="Element Offset", default=0)
|
||||
element_limit: IntProperty(name="Element Offset", default=30000)
|
||||
load_indexed_maps: BoolProperty(
|
||||
name="Load Indexed Maps",
|
||||
description="Load indexed maps (UV and color maps)",
|
||||
name="Load Indexed Colour Maps",
|
||||
description="Load indexed colour maps (vertex colours). Texture UV maps are always loaded",
|
||||
default=False, # Very slow and hackishly implemented
|
||||
)
|
||||
links: CollectionProperty(name="Links", type=Link)
|
||||
|
||||
@@ -243,6 +243,42 @@ The Styles panel is used to manage presentation styles for elements, including s
|
||||
curve styles (line color, thickness, etc.), and fill area styles. These styles control how elements are visually represented in various contexts,
|
||||
such as in 2D drawings or 3D views.
|
||||
|
||||
Textures
|
||||
--------
|
||||
|
||||
Bonsai loads textures defined with ``IfcSurfaceStyleWithTextures``, as long as the surface style also
|
||||
contains an ``IfcSurfaceStyleRendering``. All three texture sources defined by the IFC schema are supported,
|
||||
and all of them are verified against the official buildingSMART
|
||||
`tessellated shape with style <https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/annex_e/tessellated-shape-with-style/content.html>`__
|
||||
examples:
|
||||
|
||||
- ``IfcImageTexture``: the image is loaded from ``URLReference``. Relative URLs are resolved
|
||||
against the location of the IFC file.
|
||||
- ``IfcBlobTexture``: the raster data embedded in the IFC file is decoded into an image.
|
||||
- ``IfcPixelTexture``: the pixel array (1 to 4 colour components) is converted into an image.
|
||||
|
||||
Texture coordinates are applied as follows:
|
||||
|
||||
- ``IfcIndexedTriangleTextureMap`` and ``IfcIndexedPolygonalTextureMap`` are loaded into the mesh UV layer
|
||||
automatically. Elements with openings are an exception: their triangulation no longer matches the
|
||||
original faceset, so indexed UVs are skipped for them.
|
||||
- ``IfcTextureCoordinateGenerator`` with ``COORD`` and ``COORD-EYE`` modes maps to Blender's generated
|
||||
and camera texture coordinates respectively.
|
||||
- ``IfcTextureMap`` is not supported.
|
||||
|
||||
Which texture ``Mode`` is honoured depends on the ``ReflectanceMethod`` of the rendering style:
|
||||
|
||||
- ``PHYSICAL`` and ``NOTDEFINED``: ``DIFFUSE``, ``NORMAL``, ``EMISSIVE``, ``METALLICROUGHNESS`` and
|
||||
``OCCLUSION``.
|
||||
- ``FLAT``: ``EMISSIVE`` only.
|
||||
|
||||
``TextureTransform`` (texture scale and rotation) is not supported yet. Indexed colour maps
|
||||
(``IfcIndexedColourMap``, vertex colours without a texture) are only loaded when the
|
||||
"Load Indexed Colour Maps" advanced project load setting is enabled.
|
||||
|
||||
Note that the viewport must use Material Preview or Rendered shading (or Solid shading with the color
|
||||
set to Texture) for textures to be visible.
|
||||
|
||||
Profiles
|
||||
--------
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Texture UV maps must load on a plain project load (issue #6702).
|
||||
|
||||
The official buildingSMART texture examples (blob/image/pixel texture on a
|
||||
tessellated shape) rendered untextured in Bonsai because the mesh UV layer
|
||||
from IfcIndexedTriangleTextureMap was only applied behind the
|
||||
"Load Indexed Colour Maps" advanced setting (default off). Texture UVs are
|
||||
now always loaded; the setting only gates indexed colour maps."""
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.aggregate
|
||||
import ifcopenshell.api.context
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.api.unit
|
||||
import ifcopenshell.util.unit
|
||||
from mathutils import Vector
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
from . import bootstrap
|
||||
|
||||
# One quad split in two triangles, textured with a 2x2 pixel texture.
|
||||
VERTS = ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0))
|
||||
TRIANGLES = ((1, 2, 3), (1, 3, 4))
|
||||
UV_COORDS = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))
|
||||
TEX_COORD_INDEX = ((1, 2, 3), (1, 3, 4))
|
||||
|
||||
|
||||
def create_textured_ifc(path: str) -> None:
|
||||
ifc = ifcopenshell.file(schema="IFC4")
|
||||
project = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject", name="Test")
|
||||
ifcopenshell.api.unit.assign_unit(ifc)
|
||||
model = ifcopenshell.api.context.add_context(ifc, context_type="Model")
|
||||
body = ifcopenshell.api.context.add_context(
|
||||
ifc, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
|
||||
)
|
||||
|
||||
site = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSite", name="Site")
|
||||
ifcopenshell.api.aggregate.assign_object(ifc, products=[site], relating_object=project)
|
||||
element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcBuildingElementProxy", name="Textured")
|
||||
ifcopenshell.api.spatial.assign_container(ifc, products=[element], relating_structure=site)
|
||||
|
||||
coordinates = ifc.create_entity("IfcCartesianPointList3D", CoordList=VERTS)
|
||||
face_set = ifc.create_entity("IfcTriangulatedFaceSet", Coordinates=coordinates, CoordIndex=TRIANGLES)
|
||||
representation = ifc.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
ContextOfItems=body,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="Tessellation",
|
||||
Items=[face_set],
|
||||
)
|
||||
ifcopenshell.api.geometry.assign_representation(ifc, product=element, representation=representation)
|
||||
ifcopenshell.api.geometry.edit_object_placement(ifc, product=element)
|
||||
|
||||
# 2x2 RGBA pixel texture, self-contained (no external image file needed).
|
||||
texture = ifc.create_entity(
|
||||
"IfcPixelTexture",
|
||||
RepeatS=True,
|
||||
RepeatT=True,
|
||||
Mode="DIFFUSE",
|
||||
Width=2,
|
||||
Height=2,
|
||||
ColourComponents=4,
|
||||
# Bit-string encoding of RGBA pixels: red, green, blue, white.
|
||||
Pixel=[
|
||||
"".join(f"{byte:08b}" for byte in rgba)
|
||||
for rgba in ((255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255), (255, 255, 255, 255))
|
||||
],
|
||||
)
|
||||
uv_verts = ifc.create_entity("IfcTextureVertexList", TexCoordsList=UV_COORDS)
|
||||
ifc.create_entity(
|
||||
"IfcIndexedTriangleTextureMap",
|
||||
Maps=[texture],
|
||||
MappedTo=face_set,
|
||||
TexCoords=uv_verts,
|
||||
TexCoordIndex=TEX_COORD_INDEX,
|
||||
)
|
||||
|
||||
rendering = ifc.create_entity(
|
||||
"IfcSurfaceStyleRendering",
|
||||
SurfaceColour=ifc.create_entity("IfcColourRgb", Red=1.0, Green=1.0, Blue=1.0),
|
||||
ReflectanceMethod="NOTDEFINED",
|
||||
)
|
||||
textures_style = ifc.create_entity("IfcSurfaceStyleWithTextures", Textures=[texture])
|
||||
style = ifc.create_entity("IfcSurfaceStyle", Name="Textured style", Side="BOTH", Styles=[rendering, textures_style])
|
||||
ifc.create_entity("IfcStyledItem", Item=face_set, Styles=[style], Name=None)
|
||||
|
||||
ifc.write(path)
|
||||
|
||||
|
||||
class TestTextureUVImport(bootstrap.NewFile):
|
||||
def test_texture_uvs_load_without_indexed_maps_setting(self, tmp_path):
|
||||
ifc_path = str(tmp_path / "textured.ifc")
|
||||
create_textured_ifc(ifc_path)
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
assert not props.load_indexed_maps # texture UVs must not depend on this setting
|
||||
bpy.ops.bim.load_project(filepath=ifc_path, should_start_fresh_session=False)
|
||||
|
||||
obj = bpy.data.objects.get("IfcBuildingElementProxy/Textured")
|
||||
assert obj is not None
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
|
||||
# UV layer applied from IfcIndexedTriangleTextureMap.
|
||||
uv_layer = mesh.uv_layers.active
|
||||
assert uv_layer is not None
|
||||
expected = []
|
||||
for triangle, tex_coord_index in zip(TRIANGLES, TEX_COORD_INDEX):
|
||||
for vert_index, uv_index in zip(triangle, tex_coord_index):
|
||||
expected.append((VERTS[vert_index - 1], UV_COORDS[uv_index - 1]))
|
||||
assert len(uv_layer.data) == len(expected)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for poly in mesh.polygons:
|
||||
for loop_index in poly.loop_indices:
|
||||
vert = mesh.vertices[mesh.loops[loop_index].vertex_index]
|
||||
matches = [uv for co, uv in expected if (Vector(co) * unit_scale - vert.co).length < 1e-5]
|
||||
uv = uv_layer.data[loop_index].uv
|
||||
assert any((Vector(m) - uv).length < 1e-5 for m in matches)
|
||||
|
||||
# Material has the decoded pixel texture wired to Base Color.
|
||||
material = next((m for m in mesh.materials if m), None)
|
||||
assert material is not None and material.use_nodes
|
||||
image_node = next(n for n in material.node_tree.nodes if n.type == "TEX_IMAGE")
|
||||
assert image_node.image is not None
|
||||
assert tuple(image_node.image.size) == (2, 2)
|
||||
(link,) = image_node.outputs[0].links
|
||||
assert link.to_socket.name == "Base Color"
|
||||
Reference in New Issue
Block a user