Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions newton/_src/sensors/warp_raytrace/textures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]
Expand Down
9 changes: 6 additions & 3 deletions newton/_src/utils/import_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading