Skip to content

Warp Renderer OpenGL Lighting Parity#1364

Merged
thowell merged 3 commits into
google-deepmind:mainfrom
mhasek:renderer-lighting-parity
Jun 2, 2026
Merged

Warp Renderer OpenGL Lighting Parity#1364
thowell merged 3 commits into
google-deepmind:mainfrom
mhasek:renderer-lighting-parity

Conversation

@mhasek

@mhasek mhasek commented May 19, 2026

Copy link
Copy Markdown
Contributor

PR Description: Renderer OpenGL Lighting Parity

Summary

This PR brings the MuJoCo Warp RGB renderer much closer to MuJoCo OpenGL lighting behavior. It plumbs missing light and material fields into types.Model, rewrites compute_lighting around MuJoCo-style diffuse/specular/attenuation/spotlight terms, injects the MuJoCo headlight as a camera-relative light, and adds opt-out flags for the main parity features.

The renderer now handles:

  • MuJoCo vis.headlight ambient/diffuse/specular contribution.
  • Per-light diffuse, specular, ambient, attenuation, cutoff, and exponent.
  • Material specular, shininess, and emission.
  • Binary shadow visibility that matches OpenGL more closely. (Not sure if this should be configurable)
  • Black-background parity in the renderer parity tests.
  • Unit tests that look at a simplified SSIM score diff between opengl and warp renders

I realize this is on the longer side and I put this all in a single PR but if need be I can split up into subcomponents to ease the processes.

Visual Parity

Canonical fixture SSIM against mujoco.Renderer improves from roughly 0.3-0.5 before this PR to 0.917-0.994 after this PR.

Renderer parity fixtures before/after/reference

The grid below shows each renderer parity test fixture before this PR, after this PR, and against the MuJoCo OpenGL reference.
image

Feature visual impact by opt-out flag

The visual comparison below shows the effect of disabling each feature independently on a representative lit scene, with shadows disabled to isolate the lighting terms.
image

Performance Cost

A reference scene was evaluated for 100 samples where the impact of each feature on SSIM and compute is shown compared to the baseline (origin/main)
image

Reference Scene

"""
<mujoco>
  <visual>
    <headlight active="1" ambient="0.2 0.2 0.2" diffuse="0.7 0.7 0.7" specular="0.8 0.8 0.8"/>
  </visual>
  <asset>
    <material name="benchmat" specular="0.8" shininess="0.7" emission="0.1" rgba="0.6 0.7 0.9 1"/>
  </asset>
  <worldbody>
    <camera name="cam" pos="0 -3 0.9" xyaxes="1 0 0 0 0.3 1" resolution="256 256"/>
    <light pos="-1 -1.5 2" dir="0.3 0.2 -1" cutoff="35" exponent="15"
           attenuation="1 0.2 0.12" diffuse="1 1 1" specular="1 1 1" ambient="0.1 0.1 0.1"/>
    <geom type="plane" size="4 4 0.1" rgba="0.8 0.8 0.8 1"/>
    <geom type="box" pos="-0.5 0 0.15" size="0.15 0.15 0.15" material="benchmat"/>
    <geom type="sphere" pos="0.0 0 0.25" size="0.25" material="benchmat"/>
    <geom type="capsule" fromto="0.4 -0.2 0.1 0.8 0.3 0.5" size="0.08" material="benchmat"/>
  </worldbody>
</mujoco>
"""

@thowell

thowell commented May 19, 2026

Copy link
Copy Markdown
Collaborator

@mhasek thanks for adding these features to the batch renderer! the visual comparisons are great and the benchmark results are helpful for understanding the performance of the different features.

@thowell thowell requested a review from StafaH May 19, 2026 09:24

@tkelestemur tkelestemur left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left a few comments. thanks @mhasek

Comment thread mujoco_warp/_src/io.py Outdated

bvh_ngeom = len(geom_enabled_idx)

headlight_on = bool(mjm.vis.headlight.active) and headlight_active

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need renderer-level argument for this? Can't we just use the mjm.vis.headlight.active for turning the headlight on and off?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do either @thowell which one would you guys perfer? I could set it up where the user has absolute control over it or if it is just pulled from the model.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let just use the mjmodel field. thanks!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also depends on downstream usage too. In mjlab for example, would they be able to add this option easily into their existing spec/compile from config method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/io.py Outdated
Comment thread mujoco_warp/_src/io.py Outdated
bvh_ngeom = len(geom_enabled_idx)

headlight_on = bool(mjm.vis.headlight.active) and headlight_active
hl_ambient = np.asarray(mjm.vis.headlight.ambient, dtype=np.float32)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name these as headlight_ambient_mjm, headlight_diffuse_mjm etc..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/render.py Outdated
@mhasek mhasek changed the title Waro Renderer OpenGL Lighting Parity Warp Renderer OpenGL Lighting Parity May 19, 2026
@mhasek mhasek force-pushed the renderer-lighting-parity branch from 93601c9 to fa2e0c2 Compare May 20, 2026 15:46
Comment thread mujoco_warp/_src/io.py Outdated
False the term is dropped at compile time. Disable for performance
when no emission is present.
enable_per_light_ambient: When ambient lighting is enabled, sum each
light's XML `ambient` color into shaded pixels

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets remove 'XML' (could be from mjspec?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/io_test.py Outdated
Comment thread mujoco_warp/_src/io_test.py Outdated
attenuation="1 0.1 0.05" cutoff="25" exponent="10"
ambient="0.1 0.1 0.1" diffuse="0.6 0.5 0.4" specular="0.7 0.7 0.7"
bulbradius="0.05"/>
<light name="dir" pos="0 0 5" dir="0 0 -1" directional="true"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove name?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/render.py Outdated
Comment thread mujoco_warp/_src/render.py Outdated
attenuation = attenuation * spot_factor
if not default_attenuation:
denom = lightatten[0] + lightatten[1] * dist_to_light + lightatten[2] * dist_to_light * dist_to_light
atten = 1.0 / wp.max(denom, 1.0e-6)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use math.safe_div here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/render.py Outdated
Comment thread mujoco_warp/_src/render.py Outdated
Comment thread mujoco_warp/_src/render.py Outdated

# Apply lighting and shadows
if wp.static(rc.headlight_active):
result = result + wp.cw_mul(base_color, wp.static(rc.headlight_ambient))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need wp.static here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think you do since, other wise it wont compile

Comment thread mujoco_warp/_src/render.py Outdated
view_dir,
mat_spec,
mat_shin_exp,
wp.static(rc.enable_backface_culling),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need wp.static here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we do since it accesses variables from the rc

mat_spec,
mat_shin_exp,
wp.static(rc.enable_backface_culling),
wp.static(rc.enable_specular),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need wp.static here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we do

mat_shin_exp,
wp.static(rc.enable_backface_culling),
wp.static(rc.enable_specular),
wp.static(rc.light_attenuation_is_default),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need wp.static here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you do

wp.static(rc.enable_backface_culling),
wp.static(rc.enable_specular),
wp.static(rc.light_attenuation_is_default),
wp.static(rc.has_spot_lights),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need wp.static here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you do since it comes from the rc

Comment thread mujoco_warp/_src/render.py Outdated
Comment thread mujoco_warp/_src/render.py Outdated
# Apply Headlight
if wp.static(rc.headlight_active):
cam_pos = ray_origin_world
cam_fwd = wp.vec3(-cam_mat_world[0, 2], -cam_mat_world[1, 2], -cam_mat_world[2, 2])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does -cam_mat_world[:, 2] work here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to work

wp.vec3(1.0, 0.0, 0.0),
0.0,
0.0,
wp.static(rc.headlight_diffuse),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is wp.static necessary here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately it is

0.0,
0.0,
wp.static(rc.headlight_diffuse),
wp.static(rc.headlight_specular),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is wp.static necessary here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it is

view_dir,
mat_spec,
mat_shin_exp,
wp.static(rc.enable_backface_culling),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is wp.static necessary here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it is

mat_spec,
mat_shin_exp,
wp.static(rc.enable_backface_culling),
wp.static(rc.enable_specular),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is wp.static necessary here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it is

Comment thread mujoco_warp/_src/render_test.py Outdated
Comment thread mujoco_warp/_src/render_test.py Outdated
)

@classmethod
def _fixture(cls, name: str) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets remove this and provide xmls directly to test_data.fixture

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread mujoco_warp/_src/render_test.py Outdated
Comment thread mujoco_warp/_src/render_test.py Outdated
Comment thread mujoco_warp/_src/types.py Outdated
shaded pixel. When False the term is dropped at compile time.
enable_per_light_ambient: when True and `use_ambient_lighting` is also
True, sum the per-light `light_ambient` colors into each shaded pixel
even when N.L == 0 or the pixel is shadowed. When False the second

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is 'N.L'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its the dot product between the surface normal vector and the Light vector.

@thowell

thowell commented May 21, 2026

Copy link
Copy Markdown
Collaborator

would have expected the 'before' and 'all features off' timing results to be about the same since the features should all be statically guarded? are there new features that are not fully guarded with wp.static?

@thowell

thowell commented May 21, 2026

Copy link
Copy Markdown
Collaborator

@mhasek left some comments throughout the code, great work! please let us know if you have any questions. thanks!

@mhasek mhasek left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to address all the comments. @thowell thanks for the quick review I know it was on the longer side.

Comment thread mujoco_warp/_src/io.py Outdated
bvh_ngeom = len(geom_enabled_idx)

headlight_on = bool(mjm.vis.headlight.active) and headlight_active
hl_ambient = np.asarray(mjm.vis.headlight.ambient, dtype=np.float32)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/io.py Outdated
Comment thread mujoco_warp/_src/io_test.py Outdated
Comment thread mujoco_warp/_src/io_test.py Outdated
attenuation="1 0.1 0.05" cutoff="25" exponent="10"
ambient="0.1 0.1 0.1" diffuse="0.6 0.5 0.4" specular="0.7 0.7 0.7"
bulbradius="0.05"/>
<light name="dir" pos="0 0 5" dir="0 0 -1" directional="true"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/render.py Outdated
Comment thread mujoco_warp/_src/render_test.py Outdated
Comment thread mujoco_warp/_src/render_test.py Outdated
Comment thread mujoco_warp/_src/render_test.py Outdated
Comment thread mujoco_warp/_src/types.py Outdated
shaded pixel. When False the term is dropped at compile time.
enable_per_light_ambient: when True and `use_ambient_lighting` is also
True, sum the per-light `light_ambient` colors into each shaded pixel
even when N.L == 0 or the pixel is shadowed. When False the second

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its the dot product between the surface normal vector and the Light vector.

Comment thread mujoco_warp/_src/render_test.py Outdated
)

@classmethod
def _fixture(cls, name: str) -> str:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@mhasek

mhasek commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

would have expected the 'before' and 'all features off' timing results to be about the same since the features should all be statically guarded? are there new features that are not fully guarded with wp.static?

As would I, I attempted to guard as much as possible but the light accumulation is now in the rgb space before it was a scalar my guess is that is what is contributing to this. I can attempt to guard that too but I feel it would convolute the lighting accumulation.

@StafaH StafaH left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes! The rendered output definitely looks better. Also great PR description!

Major comment on the tests, I think the added tests are excessive and also unnecessary. For testing if the flags are being passed through correctly, there are tests in io.py that you can follow or add to that can make sure that setting the flags for these lighting options is getting passed through correctly. See similar tests for render_rgb, use_textures etc (test_render_context_with_textures). They are super lightweight and just check that the options are being passed through correctly.

Regarding the actual rendering tests, I would just skip them. The rendering output here is subjective, so if we all agree that the new output looks better or closer to MuJoCo, then we can ship it. In the past we have only added rendering tests for depth and segmentation since we want strict equality in output. This renderer will never be able to match a full renderer from MuJoCo so adding alot of test overhead just to measure how similar the images are is not necessary (which is why we never added these type of tests before as well). I am of the opinion that we can just remove these tests, keep testing lightweight.

Comment thread mujoco_warp/_src/io.py Outdated

bvh_ngeom = len(geom_enabled_idx)

headlight_on = bool(mjm.vis.headlight.active) and headlight_active

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also depends on downstream usage too. In mjlab for example, would they be able to add this option easily into their existing spec/compile from config method?

Comment thread mujoco_warp/_src/io.py Outdated
Comment thread mujoco_warp/_src/io_test.py Outdated

@mhasek mhasek left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@StafaH Thanks for the review! I tried to address the comments. My only question is I remvoed the additions to the render tests but should I at least and some sanity checks tests to make sure the renderer runs with the separate configs?

Comment thread mujoco_warp/_src/io.py Outdated
False the term is dropped at compile time. Disable for performance
when no emission is present.
enable_per_light_ambient: When ambient lighting is enabled, sum each
light's XML `ambient` color into shaded pixels

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/io_test.py Outdated
Comment thread mujoco_warp/_src/io.py Outdated

bvh_ngeom = len(geom_enabled_idx)

headlight_on = bool(mjm.vis.headlight.active) and headlight_active

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread mujoco_warp/_src/io.py Outdated

@mhasek mhasek left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@StafaH Thanks for the review! I tried to address the comments. My only question is I removed the additions to the render tests but should I at least and some sanity checks tests to make sure the renderer runs with the separate configs?

Comment thread mujoco_warp/_src/io_test.py Outdated
@mhasek mhasek requested review from StafaH and thowell May 28, 2026 23:26

@StafaH StafaH left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, much cleaner. Thanks!

Comment thread mujoco_warp/_src/render.py Outdated
if mat_id_for_spec >= 0:
if wp.static(rc.enable_specular):
mat_spec = mat_specular[worldid % mat_specular.shape[0], mat_id_for_spec]
mat_shin_exp = mat_shininess[worldid % mat_shininess.shape[0], mat_id_for_spec] * 128.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the max shininess value is 128.0, should we add a constant about for this setting and then use the constant here? something like MAX_SHININESS = 128.0 and then DEFAULT_SHININESS_EXPONENT = 0.5 * MAX_SHININESS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the 128.0 here be replaced with MAX_SHININESS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, I missed that it is fixed now

@thowell

thowell commented May 29, 2026

Copy link
Copy Markdown
Collaborator

@mhasek left a comment about potentially adding a constant for max shininess, otherwise lgtm

@StafaH thanks for the review!

…th opengl renderer

make background color controllable in create_render_context api

make light params batchable

make material params batchable

pr comments

PR comments

PR comments

add render sanity tests

remove lighting field prop test

make max shininess a constant
@mhasek mhasek force-pushed the renderer-lighting-parity branch from 7e00311 to 57af4ba Compare June 1, 2026 19:19
@mhasek mhasek requested review from StafaH and thowell June 1, 2026 19:19
@mhasek

mhasek commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@thowell @StafaH Awesome speed up PR! I just rebased the branch fixed the conflicts from it can you guys take a look at it and then merge if its good to go?

@thowell thowell merged commit e65a72e into google-deepmind:main Jun 2, 2026
14 checks passed
@thowell

thowell commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

great work, thanks @mhasek!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants