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 (9dff1441d7 disabled 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 by 9dff1441d7, 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:
Petru Conduraru
2026-07-21 15:27:07 +03:00
parent e52e5e2e58
commit fc82b71fe0
5 changed files with 192 additions and 12 deletions
+4 -2
View File
@@ -84,8 +84,10 @@ 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:
self.load_texture_maps(shape_has_openings)
# 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)
del self.mesh["ios_materials"]
@@ -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"
+2 -2
View File
@@ -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"