Skip to content

Commit 3b22397

Browse files
authored
fix: bit-exact camera-feed round-trip through Filmic (#60)
1 parent 39f48df commit 3b22397

6 files changed

Lines changed: 33 additions & 20 deletions

File tree

Binary file not shown.

packages/vto-core-native/android/src/main/java/eu/alan/vto/core/VTORenderer.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,11 @@ class VTORenderer(private val context: Context) {
155155
// Configure view
156156
view.isPostProcessingEnabled = true
157157

158-
// Use a Filmic tone mapper instead of the default ACES — Filmic
159-
// compresses bright highlights more, taming the shine on the
160-
// glasses frame coming off the IBL. NOTE: the camera material's
161-
// `inverseTonemapSRGB` is the inverse of ACES, not Filmic, so the
162-
// passthrough roundtrip is no longer exact; mid-tones look fine,
163-
// bright highlights in the camera feed will sit slightly darker.
158+
// Filmic tone mapper — compresses bright highlights more than
159+
// the default ACES, taming the shine on the metallic glasses
160+
// frame off the IBL. The camera material inverts this exact
161+
// curve + the piecewise sRGB encoder so the feed round-trips
162+
// unchanged.
164163
colorGrading = ColorGrading.Builder()
165164
.toneMapper(ToneMapper.Filmic())
166165
.build(engine)

packages/vto-core-native/assets/materials/camera_background.android.mat

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,18 @@ fragment {
2424
// It still reads visibly brighter than iOS's ARKit feed, so we scale down to match.
2525
// Tweak this value if a future device calibration drifts.
2626
color.rgb *= 0.90;
27-
// Camera feed is already display-encoded; the post-processing pass
28-
// would re-tone-map it and blow out highlights. Pre-invert the
29-
// ACES-to-sRGB curve so the round-trip is a no-op.
30-
color.rgb = inverseTonemapSRGB(color.rgb);
27+
// Camera feed is already display-encoded; pre-invert the view's
28+
// (Filmic tonemap + sRGB encoder) so the round-trip is exact.
29+
// Filament's `inverseTonemapSRGB` uses pow(x, 2.2) for the
30+
// gamma step, which doesn't match the piecewise sRGB encoder on
31+
// output — we use the real piecewise sRGB→linear here, then the
32+
// analytical Filmic inverse.
33+
vec3 sRGBToLinear = mix(
34+
color.rgb / 12.92,
35+
pow((color.rgb + 0.055) / 1.055, vec3(2.4)),
36+
step(0.04045, color.rgb)
37+
);
38+
color.rgb = inverseTonemap(sRGBToLinear);
3139
material.baseColor = color;
3240
}
3341
}

packages/vto-core-native/assets/materials/camera_background.ios.mat

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,18 @@ fragment {
3131
prepareMaterial(material);
3232
vec2 uv = getUV0();
3333
vec4 color = texture(materialParams_cameraFeed, uv);
34-
// Camera feed is already display-encoded; the post-processing pass
35-
// would re-tone-map it and blow out highlights. Pre-invert the
36-
// ACES-to-sRGB curve so the round-trip is a no-op.
37-
color.rgb = inverseTonemapSRGB(color.rgb);
34+
// Camera feed is already display-encoded; pre-invert the view's
35+
// (Filmic tonemap + sRGB encoder) so the round-trip is exact.
36+
// Filament's `inverseTonemapSRGB` uses pow(x, 2.2) for the
37+
// gamma step, which doesn't match the piecewise sRGB encoder on
38+
// output — we use the real piecewise sRGB→linear here, then the
39+
// analytical Filmic inverse.
40+
vec3 sRGBToLinear = mix(
41+
color.rgb / 12.92,
42+
pow((color.rgb + 0.055) / 1.055, vec3(2.4)),
43+
step(0.04045, color.rgb)
44+
);
45+
color.rgb = inverseTonemap(sRGBToLinear);
3846
material.baseColor = color;
3947
}
4048
}

packages/vto-core-native/ios/VTORendererBridge.mm

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,10 @@ - (void)initializeWithModelUrl:(NSString *)modelUrl {
131131
// Configure view
132132
_filamentView->setPostProcessingEnabled(true);
133133

134-
// Use a Filmic tone mapper instead of the default ACES — Filmic
135-
// compresses bright highlights more, taming the shine on the
136-
// glasses frame coming off the IBL. NOTE: the camera material's
137-
// `inverseTonemapSRGB` is the inverse of ACES, not Filmic, so the
138-
// passthrough roundtrip is no longer exact; mid-tones look fine,
139-
// bright highlights in the camera feed will sit slightly darker.
134+
// Filmic tone mapper — compresses bright highlights more than the
135+
// default ACES, taming the shine on the metallic glasses frame off
136+
// the IBL. The camera material inverts this exact curve + the
137+
// piecewise sRGB encoder so the feed round-trips unchanged.
140138
{
141139
FilmicToneMapper filmic;
142140
_colorGrading = ColorGrading::Builder()
Binary file not shown.

0 commit comments

Comments
 (0)