SSContactShadowsPlugin (Screen Space Contact Shadows Plugin)
Screen Space Contact Shadows Plugin enhances traditional shadow maps by adding detailed contact shadows where objects meet surfaces, using screen-space ray-tracing to create soft, realistic shadows at contact points.
The SSContactShadowsPlugin extends the shadow system by tracing rays from lit surfaces toward light sources through the depth buffer. This creates subtle, ambient occlusion-like shadows at contact areas that standard shadow maps often miss, significantly improving visual realism especially for small details and object-ground intersections.
Features
- Screen-Space Ray Tracing: Traces shadow rays through depth buffer for contact areas
- Enhances Existing Shadows: Works alongside traditional shadow maps, not as a replacement
- Configurable Radius: Control the size of the contact shadow search area
- Adjustable Intensity: Fine-tune shadow strength for artistic control
- Per-Material Control: Enable or disable contact shadows for individual materials
- Minimal Performance Impact: Efficient ray-marching with configurable step count
- GBuffer Integration: Leverages depth data for accurate ray-tracing
- Progressive Rendering: Supports temporal noise patterns for smoother results
- Debug Mode: Visualize only contact shadows for tuning
- Light-Aware: Respects all directional and spot light directions
Installation
This plugin is part of the @threepipe/webgi-plugins package:
npm install @threepipe/webgi-pluginsBasic Setup
import {ThreeViewer, GBufferPlugin, BaseGroundPlugin} from 'threepipe'
import {SSContactShadowsPlugin} from '@threepipe/webgi-plugins'
const viewer = new ThreeViewer({
canvas: document.getElementById('canvas'),
msaa: true,
plugins: [GBufferPlugin, BaseGroundPlugin]
})
// Add contact shadows plugin
const sscs = viewer.addPluginSync(new SSContactShadowsPlugin())
// Enable stable noise for cleaner results
viewer.renderManager.stableNoise = true
// Add a directional light with shadows
const light = new DirectionalLight2(0xffffff, 4)
light.position.set(2, 2, 2)
light.castShadow = true
viewer.scene.addObject(light)
// Load model - contact shadows appear automatically
await viewer.load('model.glb')With this setup, objects will show enhanced contact shadows where they meet other surfaces, creating more realistic and grounded appearance.
Configuration
Basic Parameters
Control the appearance and quality of contact shadows:
// Radius: size of the shadow search area (0.0001-0.1)
sscs.radius = 0.015 // Default, subtle contact shadows
sscs.radius = 0.030 // Larger, softer shadows
sscs.radius = 0.005 // Tighter, sharper shadows
// Intensity: shadow strength (0-1)
sscs.intensity = 1.0 // Default, full strength
sscs.intensity = 0.5 // Subtle, light shadows
sscs.intensity = 1.0 // Strong, dramatic shadows
// Tolerance: ray intersection threshold (0.1-5)
sscs.tolerance = 1.5 // Default
sscs.tolerance = 0.5 // More precise (fewer artifacts)
sscs.tolerance = 3.0 // More forgiving (may have artifacts)The radius parameter is the most important for controlling the look:
- Smaller radius: Tight, hard-edged contact shadows (faster)
- Larger radius: Soft, ambient occlusion-like shadows (slower)
Quality Settings
Adjust the ray-tracing quality:
// Step count: ray-marching steps (1-8)
sscs.stepCount = 2 // Default, fast
sscs.stepCount = 4 // Higher quality
sscs.stepCount = 1 // Fastest, may have gaps
// Each step increases the search distance but adds computationMore steps improve shadow continuity but increase rendering cost. The default of 2 steps provides a good balance.
Debug Mode
Visualize only contact shadows for tuning:
// Show only contact shadows (no regular lighting)
sscs.onlySSCSDebug = true
// This displays:
// - White: No contact shadow
// - Dark: Strong contact shadow
// - Useful for adjusting radius and intensityPer-Material Control
Disable contact shadows for specific materials:
// Disable contact shadows on a material
material.userData.sscsDisabled = true
// Re-enable
material.userData.sscsDisabled = false
// Changes require material update
material.setDirty()This is useful for:
- Transparent or translucent materials
- UI elements or overlays
- Materials that shouldn't cast contact shadows
- Performance optimization on distant objects
Advanced Usage
Lighting Setup
Contact shadows work best with properly configured lights:
import {DirectionalLight2, PCFSoftShadowMap} from 'threepipe'
// Create directional light with shadows
const light = new DirectionalLight2(0xffffff, 4)
light.position.set(2, 2, 2)
light.lookAt(0, 0, 0)
// Shadow map configuration
light.castShadow = true
light.shadowMapSize.setScalar(1024) // Or 2048 for higher quality
light.shadowNear = 0.1
light.shadowFar = 10
light.shadowFrustum = 4
// Use soft shadow map for better blending
viewer.renderManager.renderer.shadowMap.type = PCFSoftShadowMap
viewer.scene.addObject(light)Contact shadows enhance the existing shadow map rather than replacing it.
Ground Plane Integration
Works seamlessly with ground planes:
import {BaseGroundPlugin} from 'threepipe'
// Add ground before loading model
const ground = viewer.addPluginSync(new BaseGroundPlugin())
// Contact shadows automatically work on ground
ground.material.roughness = 0.5
ground.material.color.set(0x808080)
// Add contact shadows plugin
const sscs = viewer.addPluginSync(new SSContactShadowsPlugin())
// Objects will now show contact shadows on the groundMultiple Lights
Contact shadows work with all shadow-casting lights:
// Directional light (sun)
const sun = new DirectionalLight2(0xffffee, 3)
sun.position.set(5, 10, 5)
sun.castShadow = true
viewer.scene.addObject(sun)
// Spot light (accent)
const spot = new SpotLight2(0xffffff, 10)
spot.position.set(-2, 3, 2)
spot.castShadow = true
spot.angle = Math.PI / 6
viewer.scene.addObject(spot)
// Contact shadows enhance both lights automaticallyStable Noise for Cleaner Results
Enable stable noise for better temporal stability:
// Enable stable noise pattern
viewer.renderManager.stableNoise = true
// Combine with progressive rendering for high quality
const progressive = viewer.getPluginSync(ProgressivePlugin)
if (progressive) {
progressive.convergeMode = true
}
// Contact shadows will accumulate smoothly over framesVisualizing Shadow Maps
Debug shadow maps alongside contact shadows:
import {RenderTargetPreviewPlugin} from 'threepipe'
const preview = viewer.addPluginSync(new RenderTargetPreviewPlugin())
// Preview shadow map
const light = viewer.scene.getObjectByType(DirectionalLight2)
if (light?.shadow.map) {
preview.addTarget(
() => light.shadow.map,
'shadowMap',
true, // show in UI
true, // show preview
true // flip Y
)
}Performance Tuning by Scene Scale
Adjust parameters based on your scene size:
// Small objects (jewelry, watches)
sscs.radius = 0.005
sscs.stepCount = 2
sscs.tolerance = 0.5
// Medium objects (products, furniture)
sscs.radius = 0.015
sscs.stepCount = 2
sscs.tolerance = 1.5
// Large objects (vehicles, buildings)
sscs.radius = 0.030
sscs.stepCount = 3
sscs.tolerance = 2.0How It Works
The SSContactShadowsPlugin operates by modifying the shadow calculation in materials:
Shadow Map Evaluation: Traditional shadow map is sampled first
- Regular Three.js shadow mapping runs normally
- Shadow map provides base shadow information
Ray Origin Setup: For lit pixels, set up ray origin
- Origin is the surface point in view space
- Direction is toward the light source
Screen-Space Ray Tracing: Trace ray through depth buffer
- Configurable number of steps (typically 2-4)
- Each step checks depth buffer for intersections
- Uses scene bounding radius for adaptive sampling
- Random noise pattern avoids banding artifacts
Intersection Detection: Check if ray hits geometry
- Compares ray depth to scene depth with tolerance
- Tolerance prevents false positives
- Early termination on first hit
Shadow Modulation: Darken pixels where ray hits geometry
- Intensity controls shadow strength
- Result multiplied with existing shadow map value
- Creates darkening at contact areas
Final Composition: Combine with material lighting
- Contact shadows integrated into standard lighting pipeline
- Works with all material properties
- Respects existing shadow attenuation
The plugin enhances rather than replaces standard shadows, working alongside Three.js shadow maps.
Performance Considerations
- Step Count: Primary performance factor - keep at 2-4 for most cases
- Radius: Larger radius = more searching = slower (but usually negligible)
- Resolution: Operates at full resolution, scales with screen size
- Per-Light Cost: Runs for each shadow-casting light
- GBuffer Requirement: Needs depth buffer, minimal extra cost
Tips for optimization:
- Use
stepCount: 2for most applications (default) - Disable on materials that don't need contact shadows
- Reduce
radiusfor distant objects if needed - Consider disabling for mobile devices if performance critical
Use Cases
The SSContactShadowsPlugin is ideal for:
- Product Visualization: Enhanced contact between products and surfaces
- Jewelry & Watches: Subtle shadows on small details and contact points
- Architectural Interiors: Improved shadow definition at wall-floor junctions
- Furniture Visualization: Realistic shadows where furniture meets floors
- Character Rendering: Better ground contact for feet and props
- Technical Demos: Showcasing advanced shadow techniques
- High-Quality Renders: Adding that extra layer of realism
Limitations
Screen-space contact shadows have inherent limitations:
- Off-Screen Geometry: Cannot create shadows from objects outside the view
- Screen-Space Only: Limited to visible geometry in the depth buffer
- Small Search Radius: Not suitable for large-scale shadow simulation
- Light Direction Dependent: Quality depends on light-surface angle
- Not a Shadow Map Replacement: Enhances, doesn't replace traditional shadows
Contact shadows work best as an enhancement to existing shadow maps, not as a standalone solution.
Common Patterns
Subtle Product Enhancement
For product visualization with natural-looking shadows:
sscs.radius = 0.010
sscs.intensity = 0.8
sscs.stepCount = 2
sscs.tolerance = 1.0
// Use with soft PCF shadows
viewer.renderManager.renderer.shadowMap.type = PCFSoftShadowMapStrong Artistic Effect
For dramatic, high-contrast contact shadows:
sscs.radius = 0.025
sscs.intensity = 1.0
sscs.stepCount = 3
sscs.tolerance = 1.5
// Combine with strong directional lighting
light.intensity = 5Performance-Optimized
For mobile or lower-end devices:
sscs.radius = 0.012
sscs.intensity = 0.7
sscs.stepCount = 1
sscs.tolerance = 2.0
// Use smaller shadow map size
light.shadowMapSize.setScalar(512)Dependencies
- GBufferPlugin (required): Provides depth buffer for ray-tracing
- ProgressivePlugin (optional): Enables temporal stability
- BaseGroundPlugin (recommended): Provides ground plane for shadows
Troubleshooting
Contact shadows not visible:
- Ensure lights have
castShadow = true - Check that materials have
receiveShadow = true(default) - Verify GBufferPlugin is loaded
- Try increasing
intensityorradius - Check that
sscsDisabledis not set on materials
Artifacts or noise in shadows:
- Enable
viewer.renderManager.stableNoise = true - Reduce
radiusvalue - Adjust
tolerance(try lower values like 0.5-1.0) - Increase
stepCountfor smoother results
Performance issues:
- Reduce
stepCountto 1 or 2 - Decrease
radiusvalue - Disable contact shadows on distant or small objects
- Consider using lower shadow map resolution
Shadows too strong or dark:
- Reduce
intensity(try 0.5-0.8) - Decrease
radiusfor tighter shadows - Adjust regular shadow map settings (bias, near/far)
Shadows in wrong locations:
- Check light position and direction
- Verify shadow frustum encompasses the scene
- Adjust
toleranceparameter - Ensure scene scale matches
radiusexpectations
Examples
Check out these examples to see the plugin in action:
- SS Contact Shadows Plugin - Basic contact shadows setup with directional light
- Base Ground Plugin - Ground plane with enhanced shadows
See Also
- SSReflectionPlugin - Screen-space reflections using similar ray-tracing
- GBufferPlugin - Required depth buffer system
- BaseGroundPlugin - Ground plane for shadow receiving
- CascadedShadowsPlugin - Advanced shadow mapping for large scenes
- SSAOPlugin - Ambient occlusion for similar depth-based effects
