Pachinball now uses a centralized MaterialLibrary system with PBR (Physically Based Rendering) materials for a modern, premium look. The system provides procedural fallbacks for all textures, so the game works without any external assets.
To enhance visuals, add an environment texture:
public/
└── textures/
└── environment.env ← Optional: Enables realistic reflections
That's it! Everything else works procedurally.
The MaterialLibrary organizes materials into 6 clear categories:
File: src/game-elements/material-library.ts - getPlayfieldMaterial()
| Property | Value |
|---|---|
| Type | PBRMaterial |
| Metallic | 0.3 |
| Roughness | 0.25 |
| Alpha | 0.92 |
| Clear Coat | Enabled (0.4 intensity) |
Texture Support:
playfield_albedo.png- Base color (fallback: procedural grid)playfield_normal.png- Surface bumpsplayfield_roughness.png- Gloss variationplayfield_metallic.png- Metal areasplayfield_emissive.png- Glowing grid linesplayfield_ao.png- Ambient occlusion
Files:
getChromeMaterial()- Highly reflective chromegetBrushedMetalMaterial()- Brushed steel lookgetPinMaterial()- Pachinko pins
| Property | Chrome | Brushed | Pins |
|---|---|---|---|
| Metallic | 1.0 | 0.9 | 1.0 |
| Roughness | 0.15 | 0.4 | 0.25 |
| Clear Coat | No | No | Yes |
Files:
getSmokedGlassMaterial()- Wall barriersgetGlassTubeMaterial()- Feed tubes
| Property | Value |
|---|---|
| Alpha | 0.25-0.35 |
| IOR | 1.4-1.5 |
| Metallic | 0.0-0.1 |
Files:
getCabinetMaterial()- Main cabinet (StandardMaterial)getSidePanelMaterial()- Side panels with glowgetBlackPlasticMaterial()- Control panels (PBR)
Files:
getNeonBumperMaterial(color)- BumpersgetNeonFlipperMaterial()- FlippersgetNeonSlingshotMaterial()- SlingshotsgetHologramMaterial(color)- Wireframe holograms
Files:
getChromeBallMaterial()- Main ball (chrome)getExtraBallMaterial()- Multiball variant (green)
Path: public/textures/environment.env
This is a prefiltered DDS cubemap that provides realistic reflections for metallic surfaces.
- Uses procedural lighting
- Metallic surfaces reflect scene lights
- Slightly less realistic but still good
- Chrome ball reflects environment
- Pins have realistic highlights
- Overall more premium look
- Download HDRi from Poly Haven
- Convert using Babylon.js sandbox:
- Drag HDRi into sandbox
- Click Inspector → Tools → Generate .env
- Download the .env file
- Place at
public/textures/environment.env
All textures are optional. If present, they override procedural defaults.
public/textures/
├── environment.env # Environment cubemap
├── playfield_albedo.png # RGB - Base color
├── playfield_normal.png # RGB - Normal map
├── playfield_roughness.png # Grayscale - Roughness
├── playfield_metallic.png # Grayscale - Metallic
├── playfield_emissive.png # RGB - Emission
├── playfield_ao.png # Grayscale - Ambient Occlusion
└── brushed_metal_roughness.png # Anisotropic roughness
- Resolution: 1024x1024 or 2048x2048
- Format: PNG (lossless) for most
- Environment: .env (prefiltered cubemap)
import { getMaterialLibrary } from './game-elements/material-library'
// In your class:
private matLib = getMaterialLibrary(this.scene)
// Get materials:
const ballMat = this.matLib.getChromeBallMaterial()
const groundMat = this.matLib.getPlayfieldMaterial()// In MaterialLibrary class:
getCustomMaterial(): PBRMaterial {
const cacheKey = 'custom'
if (this.materialCache.has(cacheKey)) {
return this.materialCache.get(cacheKey) as PBRMaterial
}
const mat = new PBRMaterial('customMat', this.scene)
// ... configure material ...
this.materialCache.set(cacheKey, mat)
return mat
}private loadTextureSet(name: string): TextureSet {
return {
albedo: this.tryLoadTexture(`${name}_albedo.png`),
normal: this.tryLoadTexture(`${name}_normal.png`),
// ... etc
}
}If a texture fails to load, it returns null and the material uses fallback colors.
- PBR materials are GPU-accelerated in modern browsers
- Material caching prevents duplicate materials
- Texture caching prevents duplicate loads
- Clear coat has minimal overhead
- Environment texture loads once and is shared
- Environment texture: 1 (optional)
- Custom textures: 5-10 maximum
- Total VRAM impact: <50MB with all textures
Check console for: No environment.env found, using procedural reflections
- This is normal - the game works without it
- Add environment.env for better reflections
Check browser Network tab:
- Textures are loaded from
/textures/ - 404 errors are expected if textures aren't present (fallbacks used)
Indicates shader compilation error:
- Check browser console for WebGL errors
- Verify texture dimensions are power-of-2
Display a looped video or static image on the head-of-table backbox screen. Video takes priority; falls back to image, then to procedural reels.
- Video - If configured and loads successfully
- Static Image - If video fails or not configured
- Reels/Slots - Procedural fallback (always works)
In src/config.ts:
backbox: {
// Video (highest priority)
attractVideoPath: '/backbox/attract.mp4',
videoReplacesReels: true, // Hide reels when video plays
// Image (fallback)
attractImagePath: '/backbox/attract.png',
imageOpacity: 0.85,
imageBlendMode: 'normal',
}- Format: MP4 (H.264) for maximum compatibility
- Resolution: 1920x1080 or 1280x720 (16:9)
- Audio: Not required (video is muted for autoplay)
- Loop: Should loop seamlessly (last frame → first frame)
- File Size: Under 50MB for fast loading
- Location:
public/backbox/attract.mp4
- ✅ Autoplay (muted, inline)
- ✅ Automatic loop
- ✅ Graceful fallback on load failure
- ✅ Graceful fallback on autoplay block
- ✅ Aspect ratio preserved (letterboxed if needed)
- ✅ Scanline/overlay effects still apply on top
Used when video is not configured or fails to load:
backbox: {
attractVideoPath: '', // Disable video
attractImagePath: '/backbox/attract.png', // Use image
imageOpacity: 0.85, // 0.0 - 1.0
imageBlendMode: 'normal', // 'normal' | 'additive' | 'multiply'
}- Reels/Slots - Deepest layer (hidden if video replaces reels)
- Video - Looped video (if configured and loaded)
- Image - Static image (fallback if video fails)
- Animated Grid - Cyber grid shader (shows through transparent areas)
- UI Overlay - Text, scanlines, jackpot effects
- Resolution: 1024x512 or 1920x1080 (16:9)
- Format: PNG with transparency recommended
- Style: Dark backgrounds work best with visible grid underneath
| Mode | Effect | Best For |
|---|---|---|
normal |
Standard alpha blend | Most images |
additive |
Adds to background | Glow effects, neon art |
multiply |
Darkens background | Light/white images |
// Video controls
displaySystem.playVideo()
displaySystem.pauseVideo()
displaySystem.setVideoOpacity(0.5)
displaySystem.setVideoVisible(false)
// Image controls
displaySystem.setImageVisible(false)
displaySystem.setImageOpacity(0.5)
displaySystem.setImageOpacity(0.5)Previous code using StandardMaterial directly:
const mat = new StandardMaterial('old', this.scene)
mat.diffuseColor = Color3.Red()New code using MaterialLibrary:
const mat = this.matLib.getNeonBumperMaterial('#ff0000')Or for custom materials, use PBRMaterial:
const mat = new PBRMaterial('new', this.scene)
mat.albedoColor = Color3.Red()
mat.metallic = 0.5
mat.roughness = 0.3