diff --git a/newton/_src/sensors/warp_raytrace/textures.py b/newton/_src/sensors/warp_raytrace/textures.py index 23fbc42fa..faebd08eb 100644 --- a/newton/_src/sensors/warp_raytrace/textures.py +++ b/newton/_src/sensors/warp_raytrace/textures.py @@ -47,6 +47,47 @@ def sample_texture_mesh( return sample_texture_2d(flip_v(wp.cw_mul(uv, texture_data.repeat)), texture_data) +@wp.func +def sample_texture_triplanar( + hit_point: wp.vec3f, + shape_transform: wp.transformf, + mesh_id: wp.uint64, + face_id: wp.int32, + texture_data: TextureData, +) -> wp.vec3f: + """Triplanar texture projection for meshes without UVs (equivalent to project_uvw). + + Samples the texture from 3 axis-aligned projections and blends based on + the face normal, producing seamless results on arbitrarily oriented surfaces. + """ + # Compute face normal from mesh triangle vertices + i0 = wp.mesh_get_index(mesh_id, face_id * 3 + 0) + i1 = wp.mesh_get_index(mesh_id, face_id * 3 + 1) + i2 = wp.mesh_get_index(mesh_id, face_id * 3 + 2) + p0 = wp.mesh_get_point(mesh_id, i0) + p1 = wp.mesh_get_point(mesh_id, i1) + p2 = wp.mesh_get_point(mesh_id, i2) + face_normal = wp.normalize(wp.cross(p1 - p0, p2 - p0)) + + # Transform hit point to local object space + inv_transform = wp.transform_inverse(shape_transform) + local = wp.transform_point(inv_transform, hit_point) + + # Blending weights from absolute normal components (sum to 1) + w = wp.vec3f(wp.abs(face_normal[0]), wp.abs(face_normal[1]), wp.abs(face_normal[2])) + w_sum = w[0] + w[1] + w[2] + if w_sum > 0.0: + w = w / w_sum + + # Sample texture from 3 axis-aligned projections + repeat = texture_data.repeat + c_x = sample_texture_2d(flip_v(wp.cw_mul(wp.vec2f(local[1], local[2]), repeat)), texture_data) + c_y = sample_texture_2d(flip_v(wp.cw_mul(wp.vec2f(local[0], local[2]), repeat)), texture_data) + c_z = sample_texture_2d(flip_v(wp.cw_mul(wp.vec2f(local[0], local[1]), repeat)), texture_data) + + return c_x * w[0] + c_y * w[1] + c_z * w[2] + + @wp.func def sample_texture( shape_type: wp.int32, @@ -70,11 +111,15 @@ def sample_texture( return sample_texture_plane(hit_point, shape_transform, texture_data[texture_index]) if shape_type == GeoType.MESH: - if face_id < 0 or mesh_data_index < 0: + if face_id < 0: return DEFAULT_RETURN - if mesh_data[mesh_data_index].uvs.shape[0] == 0: - return DEFAULT_RETURN + # Triplanar projection for meshes without UV data (equivalent to project_uvw). + # Check before mesh_data_index guard since triplanar doesn't need mesh_data. + if mesh_data_index < 0 or mesh_data[mesh_data_index].uvs.shape[0] == 0: + return sample_texture_triplanar( + hit_point, shape_transform, mesh_id, face_id, texture_data[texture_index] + ) return sample_texture_mesh( bary_u, bary_v, face_id, mesh_id, mesh_data[mesh_data_index], texture_data[texture_index] diff --git a/newton/_src/utils/import_usd.py b/newton/_src/utils/import_usd.py index 2e0eae935..a355125c5 100644 --- a/newton/_src/utils/import_usd.py +++ b/newton/_src/utils/import_usd.py @@ -431,11 +431,14 @@ def _get_mesh_with_visual_material(prim: Usd.Prim, *, path_name: str) -> Mesh: mesh.texture = texture if mesh.texture is not None and mesh.uvs is None: warnings.warn( - f"Warning: mesh {path_name} has a texture but no UVs; texture will be ignored.", + f"Mesh {path_name} has a texture but no UVs; texture will use projected UVs.", stacklevel=2, ) - mesh.texture = None - if material_props.get("color") is not None and mesh.texture is None: + if mesh.texture is not None: + # Texture provides albedo; use white so it renders at full brightness. + # (diffuse_color_constant is the untextured fallback, not a multiplier.) + mesh.color = (1.0, 1.0, 1.0) + elif material_props.get("color") is not None: mesh.color = material_props["color"] if material_props.get("roughness") is not None: mesh.roughness = material_props["roughness"]