Skip to content

Commit 9a7d1f7

Browse files
authored
docs: Soft shadows & bounce lighting in the "Jelly Slider" example (#1881)
1 parent 68146db commit 9a7d1f7

File tree

2 files changed

+103
-52
lines changed

2 files changed

+103
-52
lines changed

apps/typegpu-docs/src/examples/rendering/jelly-slider/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const GROUND_ALBEDO = d.vec3f(1);
1111
// Lighting constants
1212
export const AMBIENT_COLOR = d.vec3f(0.6);
1313
export const AMBIENT_INTENSITY = 0.6;
14-
export const SPECULAR_POWER = 120.0;
14+
export const SPECULAR_POWER = 10;
1515
export const SPECULAR_INTENSITY = 0.6;
1616

1717
// Jelly material constants

apps/typegpu-docs/src/examples/rendering/jelly-slider/index.ts

Lines changed: 102 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,13 @@ const getRay = (ndc: d.v2f) => {
125125
const invView = cameraUniform.$.viewInv;
126126
const invProj = cameraUniform.$.projInv;
127127

128-
const viewPos = std.mul(invProj, clipPos);
128+
const viewPos = invProj.mul(clipPos);
129129
const viewPosNormalized = d.vec4f(viewPos.xyz.div(viewPos.w), 1.0);
130130

131-
const worldPos = std.mul(invView, viewPosNormalized);
131+
const worldPos = invView.mul(viewPosNormalized);
132132

133133
const rayOrigin = invView.columns[3].xyz;
134-
const rayDir = std.normalize(std.sub(worldPos.xyz, rayOrigin));
134+
const rayDir = std.normalize(worldPos.xyz.sub(rayOrigin));
135135

136136
return Ray({
137137
origin: rayOrigin,
@@ -224,16 +224,34 @@ const sliderSdf3D = (position: d.v3f) => {
224224
});
225225
};
226226

227+
const GroundParams = {
228+
groundThickness: 0.03,
229+
groundRoundness: 0.02,
230+
};
231+
232+
const rectangleCutoutDist = (position: d.v2f) => {
233+
'use gpu';
234+
const groundRoundness = GroundParams.groundRoundness;
235+
236+
return sdf.sdRoundedBox2d(
237+
position,
238+
d.vec2f(1 + groundRoundness, 0.2 + groundRoundness),
239+
0.2 + groundRoundness,
240+
);
241+
};
242+
227243
const getMainSceneDist = (position: d.v3f) => {
228244
'use gpu';
229-
return sdf.opSmoothDifference(
230-
sdf.sdPlane(position, d.vec3f(0, 1, 0), 0),
245+
const groundThickness = GroundParams.groundThickness;
246+
const groundRoundness = GroundParams.groundRoundness;
247+
248+
return sdf.opUnion(
249+
sdf.sdPlane(position, d.vec3f(0, 1, 0), 0.06),
231250
sdf.opExtrudeY(
232251
position,
233-
sdf.sdRoundedBox2d(position.xz, d.vec2f(1, 0.2), 0.2),
234-
0.06,
235-
),
236-
0.01,
252+
-rectangleCutoutDist(position.xz),
253+
groundThickness - groundRoundness,
254+
) - groundRoundness,
237255
);
238256
};
239257

@@ -384,50 +402,74 @@ const getNormal = (
384402
);
385403
};
386404

387-
const getShadow = (position: d.v3f, normal: d.v3f, lightDir: d.v3f) => {
405+
const sqLength = (a: d.v3f) => {
388406
'use gpu';
389-
const newDir = std.normalize(lightDir);
390-
391-
const bias = 0.005;
392-
const newOrigin = position.add(normal.mul(bias));
393-
394-
const bbox = getSliderBbox();
395-
const zDepth = d.f32(0.21);
407+
return std.dot(a, a);
408+
};
396409

397-
const sliderMin = d.vec3f(bbox.left, bbox.bottom, -zDepth);
398-
const sliderMax = d.vec3f(bbox.right, bbox.top, zDepth);
410+
const getFakeShadow = (
411+
position: d.v3f,
412+
lightDir: d.v3f,
413+
): d.v3f => {
414+
'use gpu';
415+
const jellyColor = jellyColorUniform.$;
416+
const endCapX = slider.endCapUniform.$.x;
399417

400-
const intersection = intersectBox(
401-
newOrigin,
402-
newDir,
403-
sliderMin,
404-
sliderMax,
405-
);
418+
if (position.y < -GroundParams.groundThickness) {
419+
// Applying darkening under the ground (the shadow cast by the upper ground layer)
420+
const fadeSharpness = 30;
421+
const inset = 0.02;
422+
const cutout = rectangleCutoutDist(position.xz) + inset;
423+
const edgeDarkening = std.saturate(1 - cutout * fadeSharpness);
406424

407-
if (intersection.hit) {
408-
let t = std.max(0.0, intersection.tMin);
409-
const maxT = intersection.tMax;
425+
// Applying a slight gradient based on the light direction
426+
const lightGradient = std.saturate(-position.z * 4 * lightDir.z + 1);
410427

411-
for (let i = 0; i < MAX_STEPS; i++) {
412-
const currPos = newOrigin.add(newDir.mul(t));
413-
const hitInfo = getSceneDist(currPos);
428+
return d.vec3f(1).mul(edgeDarkening).mul(lightGradient * 0.5);
429+
} else {
430+
const finalUV = d.vec2f(
431+
(position.x - position.z * lightDir.x * std.sign(lightDir.z)) *
432+
0.5 + 0.5,
433+
1 - (-position.z / lightDir.z * 0.5) - 0.2,
434+
);
435+
const data = std.textureSampleLevel(
436+
bezierTexture.$,
437+
filteringSampler.$,
438+
finalUV,
439+
0,
440+
);
414441

415-
if (hitInfo.distance < SURF_DIST) {
416-
return std.select(
417-
0.8,
418-
0.3,
419-
hitInfo.objectType === ObjectType.SLIDER,
420-
);
421-
}
442+
// Normally it would be just data.y, but there transition is too sudden when the jelly is bunched up.
443+
// To mitigate this, we transition into a position-based transition.
444+
const jellySaturation = std.mix(
445+
0,
446+
data.y,
447+
std.saturate(position.x * 1.5 + 1.1),
448+
);
449+
const shadowColor = std.mix(
450+
d.vec3f(0, 0, 0),
451+
jellyColor.xyz,
452+
jellySaturation,
453+
);
422454

423-
t += hitInfo.distance;
424-
if (t > maxT) {
425-
break;
426-
}
427-
}
455+
const contrast = 20 * std.saturate(finalUV.y) * (0.8 + endCapX * 0.2);
456+
const shadowOffset = -0.3;
457+
const featherSharpness = 10;
458+
const uvEdgeFeather = std.saturate(finalUV.x * featherSharpness) *
459+
std.saturate((1 - finalUV.x) * featherSharpness) *
460+
std.saturate((1 - finalUV.y) * featherSharpness) *
461+
std.saturate(finalUV.y);
462+
const influence = std.saturate((1 - lightDir.y) * 2) * uvEdgeFeather;
463+
return std.mix(
464+
d.vec3f(1),
465+
std.mix(
466+
shadowColor,
467+
d.vec3f(1),
468+
std.saturate(data.x * contrast + shadowOffset),
469+
),
470+
influence,
471+
);
428472
}
429-
430-
return d.f32(0);
431473
};
432474

433475
const calculateAO = (position: d.v3f, normal: d.v3f) => {
@@ -463,9 +505,7 @@ const calculateLighting = (
463505
'use gpu';
464506
const lightDir = std.neg(lightUniform.$.direction);
465507

466-
const shadow = getShadow(hitPosition, normal, lightDir);
467-
const visibility = 1.0 - shadow;
468-
508+
const fakeShadow = getFakeShadow(hitPosition, lightDir);
469509
const diffuse = std.max(std.dot(normal, lightDir), 0.0);
470510

471511
const viewDir = std.normalize(rayOrigin.sub(hitPosition));
@@ -480,10 +520,11 @@ const calculateLighting = (
480520

481521
const directionalLight = baseColor
482522
.mul(lightUniform.$.color)
483-
.mul(diffuse * visibility);
523+
.mul(diffuse)
524+
.mul(fakeShadow);
484525
const ambientLight = baseColor.mul(AMBIENT_COLOR).mul(AMBIENT_INTENSITY);
485526

486-
const finalSpecular = specular.mul(visibility);
527+
const finalSpecular = specular.mul(fakeShadow);
487528

488529
return std.saturate(directionalLight.add(ambientLight).add(finalSpecular));
489530
};
@@ -629,12 +670,22 @@ const renderBackground = (
629670
);
630671
const newNormal = getNormalMain(posOffset);
631672

673+
// Calculate fake bounce lighting
674+
const jellyColor = jellyColorUniform.$;
675+
const sqDist = sqLength(hitPosition.sub(d.vec3f(endCapX, 0, 0)));
676+
const bounceLight = jellyColor.xyz.mul(1 / (sqDist * 15 + 1) * 0.4);
677+
const sideBounceLight = jellyColor.xyz
678+
.mul(1 / (sqDist * 40 + 1) * 0.3)
679+
.mul(std.abs(newNormal.z));
680+
632681
const litColor = calculateLighting(posOffset, newNormal, rayOrigin);
633682
const backgroundColor = applyAO(
634683
GROUND_ALBEDO.mul(litColor),
635684
posOffset,
636685
newNormal,
637-
);
686+
)
687+
.add(d.vec4f(bounceLight, 0))
688+
.add(d.vec4f(sideBounceLight, 0));
638689

639690
const textColor = std.saturate(backgroundColor.xyz.mul(d.vec3f(0.5)));
640691

0 commit comments

Comments
 (0)