Tales from the Crypt¶
Tales from the Crypt is a Data East table that received one of the most technically ambitious VPW treatments, featuring a three-primitive insert system, doubled primitive GI fading with material swapping, pre-rendered flasher overlays, RTX ball shadows on wire ramps, and physical trough implementation. Development spanned approximately 6.5 months from January to July 2021 with over 100 internal versions, making it a reference implementation for many VPW techniques.
Build Notes¶
Project Base¶
The team used a VR room version as the base file rather than a raw VPX table. This provides proper VR cabinet geometry and room setup from the start, avoiding retrofit work later.
Three-Primitive Insert System¶
Advanced insert implementation using three stacked primitives:
| Layer | DL | DLFB | DB | Z | ZSize | Opacity | Purpose |
|---|---|---|---|---|---|---|---|
| OFF | 1 | 0 | 1000 | 0 | 6 | 0.99 | Surface appearance |
| ON | 0 | 0 | 1100 | 0 | 6 | 0.40 | Lit with DL=50 |
| BULB | 1 | 1 | 1200 | -3 | 30 | 0.8-0.99 | Plywood/lamp effect |
The BULB layer creates a "plywood beneath the insert" effect visible from angles and in VR. The brown area gets correct shading from the material, so for red inserts it appears more red. This method enables inserts where the insert itself is white but the LED lamp is a different color.
Insert Text¶
Using a VPX Ramp object for insert text renders sharper than flasher objects. Insert outlines can be placed on flasher objects to mask jagged playfield edges.
6K Resolution Strategy¶
Work at 6K resolution during development for all cutouts and detail work. Check prior to release whether 4K is acceptable. Working at higher resolution preserves detail and keeps cutout edges cleaner.
Playfield Alpha Mask¶
Changed alpha mask from 1 to 220 to eliminate grey borders around insert holes.
ON/OFF Primitive Organization for GI¶
Systematic approach:
- All ON prims to layer 7, depth bias 0, dedicated material
- All OFF prims to layer 9, depth bias 30, dedicated material
- Uncheck static rendering on all prims in both layers
- If "group together" is checked on a collection, VPX renders them as one object, breaking moving primitives
Texture Organization¶
All baked objects organized into 6 groups with 3000x3000 textures each. All GI_ON textures baked first, GI_OFF versions to follow.
WEBP for File Size Reduction¶
WEBP format reduced table from 620MB to 150MB (76% reduction). Requires VPX 10.7+.
VPX 10.7 Compatibility
VPX 10.7 tables are NOT backward compatible with 10.6. Saving in 10.7 totally breaks the table for 10.6 users. Only convert to 10.7 for release, not during development.
Collidable Plastics¶
Plastics with 28k polys are too heavy for collision. Use normal VPX walls (lightweight, fast) to cover areas where the ball can get stuck. Invisible walls on top of plastics prevent ball trapping.
Team Credits¶
Tomate (models/textures/ramps), iaakki (fading code, 3D inserts, PF edits, nFozzy physics, Rubberizer, TargetBouncer, Fleep sounds), Wylte (RTX ball shadows), Sixtoe (VR and fixes), Benji (nFozzy physics, Fleep sounds), apophis (RTX shadows), G5k/Skitso (lighting quality pass).
Scripting¶
GI Fading with UpdateMaterial¶
Two approaches:
- Legacy (material swaps): Select Case with
FlashLevelToIndexat discrete opacity levels. Works in VR but no stepless fading. - UpdateMaterial method: Provides stepless fading:
Per-Material-Type GI Fade Curves¶
Different materials fade at different rates for realism:
UpdateMaterial "GI_ON_CAB",0,0,0,0,0,0,aLvl^5,... 'Sideblades: fastest
UpdateMaterial "GI_ON_Plastic",0,0,0,0,0,0,aLvl^3,... 'Plastics: medium
UpdateMaterial "GI_ON_Metals",0,0,0,0,0,0,aLvl^2,... 'Metals: slower
UpdateMaterial "GI_ON_Bulbs",0,0,0,0,0,0,aLvl^0.5,... 'Bulbs: slowest
Physics insight from Flupper: incandescent bulb glow wire stays warm and emits residual light when switched off briefly.
ImageSwap Function¶
Sub ImageSwap(pri, group, DLintensity, ByVal aLvl)
if Lampz.UseFunction then aLvl = Lampz.FilterOut(aLvl)
Select case FlashLevelToIndex(aLvl, 3)
Case 1:pri.Image = group(0)
Case 2:pri.Image = group(1)
Case 3:pri.Image = group(2)
Case 4:pri.Image = group(3)
End Select
pri.blenddisablelighting = aLvl * DLintensity
End Sub
Called via: Lampz.Callback(15) = "ImageSwap prim_name, Apronimagees, 50"
Complete Flasher System Architecture¶
The flasher system handles GI state transitions:
- GI ON + flasher fires: OFF prim images swap to flasher textures, revealed by making ON prims transparent
- GI OFF + flasher fires: Must first hide OFF images by making ON prims visible with OFF images, then swap flash images, start fading, and swap back when done
Required: 6 textures per group (GI_ON, GI_OFF, RF_GI_ON, RF_GI_OFF, LF_GI_ON, LF_GI_OFF), 6 groups + 4 PF = 40 total textures.
PF GI Flasher Overlay¶
PLAYFIELD_GI1.opacity = 60
PLAYFIELD_GI1.visible = 1
PLAYFIELD_GI1.AddBlend = False
PLAYFIELD_GI1.Filter = "Overlay"
PLAYFIELD_GI1.amount = 100
Use the ON version as default and fade reverse using Screen/Overlay filter.
RTX Ball Shadow on Wire Ramps¶
Shadow scales based on ball Z height:
objBallShadow(s).size_x = 6.5 * ((BWS(s).Z-(ballsize/2))/70)
objBallShadow(s).size_y = 5.0 * ((BWS(s).Z-(ballsize/2))/70)
Combined Ball Tracking¶
Ball position tracking for shadows, rolling sounds, wire/ramp sounds, and rubber dampening was unified into a single system sharing trigger infrastructure. The ramproll timer at 100ms stops when no ball is on the ramp. The rdampen timer was reduced from 1ms to 10ms for significant CPU savings.
Performance: Removing GetBalls from Loop¶
Major improvement: define gBOT = GetBalls once globally at table init instead of calling per frame. Works because TFTC does not destroy balls. Added InRect check to skip balls under apron.
Physical Trough¶
Tables that destroy/recreate balls cause problems with ball ID tracking. Fix: implement a working physical trough that creates all balls at startup and never destroys them. Each ball keeps its name/ID for the entire session.
Slingshot Angle Corrections¶
AddSlingsPt 0, 0.00, -10
AddSlingsPt 1, 0.45, -10
AddSlingsPt 2, 0.48, 0 'dead zone center
AddSlingsPt 3, 0.52, 0
AddSlingsPt 4, 0.55, 10
AddSlingsPt 5, 1.00, 10
Ball position percentage along the sling face maps to angle correction in degrees. 10 degrees at extremes may be too aggressive -- Goldeneye used 6-7, Jokerz used ~5.
TargetBouncer¶
Applies to posts, sleeves, standup targets, and drop targets. Default ratio 1.1, max recommended 2.0. Ball jumping over plastics happens in real machines too but should be rare.
3D & Art¶
Flasher Bake Workflow¶
- Turn off ALL GI bulbs, turn on ONE flasher bulb
- Add dark grey material to playfield
- Make objects invisible to camera but still cast shadows
- Render at 2K (sufficient for overlays)
Disable Lighting on Baked Primitives¶
Set DisableLighting = 1 on all Blender-baked primitives. VPX lights clip highlights and make them go "muddy." With DL=1, textures look more natural and highlights work properly.
Z-Fighting Fix¶
Flickering between ON and OFF primitives: set OFF primitive size to 999 or 999.5. This 0.5 unit difference (~1 pixel at 2K) is imperceptible but eliminates z-fighting.
Normal Maps for Insert OFF Primitives¶
Hand-drawn normal maps add depth and dimension. Online generators produce results that are too blurry. Time-consuming but significantly improves visual quality.
Additive Flasher Technique¶
Use GI OFF image as base playfield, cast additive flasher using a difference image (ON minus OFF bake in Photoshop). Ball reflections work in all lighting states.
Troubleshooting¶
HDR Textures in VR¶
HDR textures render with bright blue hue in VPVR. Fix: export as JPG or PNG.
HDR Images Cannot Be Script-Swapped¶
VPX throws a runtime exception when swapping HDR images. Convert to PNG first (87.5MB HDR to 25.5MB PNG).
Static Rendering Breaks Image Swaps¶
If a primitive has static rendering checked, image swaps via script will not work.
Object Space Normal Maps Break VR Transparency¶
With "Object Space" selected for normal maps, transparent primitives will not render in VR. Disable Object Space.
VPX Collection "Group Together"¶
If checked, VPX renders all elements as one object. Any moving primitive in the collection stops working.
Ball Lost in Narnia (VUK Debugging)¶
Sub BallInNarnia
Dim BOT, b
BOT = GetBalls
For b = 0 to UBound(BOT)
if BOT(b).z < -200 Then
msgbox "Ball " &b& " in Narnia X: " & BOT(b).x &" Y: "&BOT(b).y
end if
next
End Sub
Ball.uservalue Automation Error¶
ActiveBall.uservalue occasionally throws "Automation type not supported" errors when balls are destroyed and recreated. Use a separate global array keyed by ball ID instead.
Kickback Plunger Type¶
TFTC uses an impulse plunger, but the VPX plunger was set to "mechanical." This caused weak kicks that drain. Fix: uncheck "Mechanical Plunger" and enable "Auto Plunger."
Captive Ball Init Timing¶
Captive ball creation at table init can crash if NF physics dampener is still initializing. Move creation to the preloader phase.
Game Knowledge¶
Two-Stage Flippers¶
Data East machines like TFTC have two-stage flipper switches. Light press actuates main flippers only. Full press also actuates upper flipper(s). The upper flipper intentionally has lower power to prevent breaking the dropdown skillshot targets.
Resources¶
Shot Debugger¶
Press 2 to block outlanes/drain. Press and hold keys to test specific shots (W, E, R, Y, U, I, P, A, S, F, G for various targets). Uses kickers that capture and kick on keypress with zero overhead.
Sound Format¶
VPX requires 16-bit WAV audio. 32-bit WAV files are silently ignored (no error, no sound). Always re-export at 16-bit.
WebP Benefits¶
WebP images are quarter the size of JPG and lossless. VPX 10.7 required. JPGs are uncompressed in VPX; WebP has some decompression overhead.