Skip to content

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:

  1. All ON prims to layer 7, depth bias 0, dedicated material
  2. All OFF prims to layer 9, depth bias 30, dedicated material
  3. Uncheck static rendering on all prims in both layers
  4. 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:

  1. Legacy (material swaps): Select Case with FlashLevelToIndex at discrete opacity levels. Works in VR but no stepless fading.
  2. UpdateMaterial method: Provides stepless fading:
UpdateMaterial "GI_ON_Material",0,0,0,0,0,0,aLvl^5,RGB(255,255,255),0,0,False,True,0,0,0,0

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

  1. Turn off ALL GI bulbs, turn on ONE flasher bulb
  2. Add dark grey material to playfield
  3. Make objects invisible to camera but still cast shadows
  4. 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.