Skip to content

Lord of the Rings (Valinor Edition)

Lord of the Rings Valinor Edition (Stern 2003) is one of the most technically ambitious VPW projects, featuring Octane-rendered baked textures, 3D-scanned figurines with aggressive poly reduction, a complex GI ON/OFF primitive fading system, RTX ball shadows, SoundCmdListener-driven mode effects, Destroy The Ring ball-ball collision mechanics, sword glow effects, flasher bloom performance optimization, VR rooms from 360-degree panoramas, and WebP image optimization. A massive collaborative effort spanning years of development.

Build Notes

Playfield Dimensions

The VPX playfield height parameter includes all surface area the ball must travel on, not just the physical plywood dimensions. When a ramp or toy extends beyond the back of the playfield (like the ring mechanism), the playfield image must be extended with extra vertical height (solid color fill) to accommodate it. Align the actual playfield art to the bottom of the oversized canvas, with extra space at the top hidden behind the back wall. Standard Stern playfield dimensions (1999+) are 20.25" x 45".

Playfield Scan Verification

When importing a new playfield scan, compare its aspect ratio against the VPX table dimensions. Round insert holes appearing oval is a telltale sign the playfield dimensions are wrong. A proper scan should match the real dimension ratio, and VPX table dimensions should be calculated from the real dimensions plus any extra height for off-playfield areas.

Lane Guides

Metal inlane lane guides implemented as primitives tend to cause occasional bugs. Replace them with VPX walls, which have more reliable collision behavior.

3D Insert Sourcing

Source insert primitives from existing VPW tables with similar shapes. Key references: F-14 Tomcat and Ghostbusters for wide arrows, Funhouse for smaller arrows/triangles, Whirlwind for Flupper's original set. Different tables may use different insert construction methods, and insert images from different people have different brightness/contrast characteristics requiring individual material and DL tuning.

Slope Calibration

To determine the correct table slope, find a reference video of the real machine where the ball drops from near the top straight down to the flippers. In VPX, reproduce the same drop at different slope settings (5.4, 5.8, 6.0, 6.5 degrees) and compare frame grabs at the same elapsed time. The slope where VPX matches the real video is correct. Playfield friction also plays a role.

Wire Ramp Collision

Visual wire ramp primitives should never be set as collidable. Create separate invisible standard VPX ramps for ball physics. Collidable visual ramps cause erratic ball behavior, lag, and stuck balls.

Playfield Alpha Mask

The playfield image alpha mask should be set to approximately 180 (not 1 or 255). All flasher images, ramp overlay images, and GI OFF images should have their alpha mask set to 1. Using incorrect values causes visible artifacts around insert cutout edges.

Preloading Flashers

Preload flasher objects (images, primitives) during table initialization to prevent lag spikes when flashers first fire during gameplay. Without preloading, the first activation of each flasher causes a stutter as VPX loads associated assets.

Layer Organization

LOTR used a multi-layer system: Layer 4 for GI-ON baked primitives, Layer 5 for GI-OFF baked primitives, Layer 7 for unique/misc prims, Layer 11 for VR assets and temporary storage. This allows toggling entire lighting states by swapping visibility between layers.

Version Control

LOTR used sequential version numbers (v041, v042, etc.) with detailed changelogs per version, shared via Dropbox and Google Drive. Each upload included a changelog line: '042 - author - description of changes. A shared Dropbox Paper document tracked the to-do list.

Cabinet Mode

Disable side blade visibility (split from cabinet model first), double the height of sidewall primitives for POV appearance. Side rails should be separate from the cab for independent toggling. Cabinet side panels were scaled to 150% height (1500 units).

Tilt Sensitivity

Digital nudging uses a timed approach since there's no physical tilt bob. Sensitivity value of 6 feels right (allows sparse nudges, warns on repeated nudges). Default 3 was too generous. Nudge strength matters more than tilt sensitivity for gameplay feel.

Scripting

SoundCmdListener for Mode Detection

ROM sound commands can be intercepted via a SoundCmdListener routine to trigger custom playfield effects tied to specific game modes. Each mode has an associated soundtrack with a unique hex sound command ID. By monitoring for these commands (e.g., 0xFD1D for DTR mode start, 0xFD2C for ring destroyed), you can activate visual effects without hacking the ROM. The hex values from the altsound CSV file match those from PinMAME's sound command mode.

DisableLighting for Glow Effects

To make a primitive glow (e.g., glowing ring text, vial of light), use a planar primitive with an alpha image and set BlendDisableLighting. For dynamic glow controlled by game events, modify the material alpha or DL value via script timers. To avoid the glow appearing during unrelated events, implement a delay (e.g., only enable after the associated insert has been lit for 3+ seconds).

When PinMAME fails to send blinking lamp signals (sending constant ON instead), implement a script-level blink hack: monitor insert lamp state with a timer, and if the lamp stays solid ON for more than 1 second, start toggling on/off. This is forward-compatible with PinMAME fixes -- real blinks won't stay solid long enough to trigger the hack's threshold.

Lampz Fade Speed Tuning

Recommended values for Stern games: FadeSpeedUp = 1/15, FadeSpeedDown = 1/70 for standard lamps, and FadeSpeedUp = 1/10, FadeSpeedDown = 1/80 for modulated lamps (ModLampz). LampTimer interval of 3ms drives the resolution.

Lampz Timer Configuration

When using LampTimer.Interval = -1 with Lampz.Update2: do NOT call both Lampz.Update and Lampz.Update2 on different timers -- causes fading to be called twice per frame, possibly out of order. Update2 is for single-timer setups; Update + Update1 are for dual-timer setups.

GI ON/OFF Primitive Fading

LOTR has a single non-modulated GI string (typical for Stern). The fading system hooks ON/OFF primitives to the Lampz lighting system. Set ON primitives to depth bias -100 and leave OFF primitives at 0 to prevent z-fighting. Testing: SetLamp 0,0 turns GI off, SetLamp 0,1 turns it on.

Skip Handling

The visibility logic can fail if the GI signal doesn't pass through intermediate fade levels. Add logic to handle direct state transitions that bypass intermediate thresholds.

2-Ball Destroy The Ring (OnBallBallCollision)

The 2-ball DTR mode requires a second ball to knock the magnetically-held ball out through the back of the ring. Using OnBallBallCollision as the trigger to reduce/disable magnet strength at the moment of impact solved timing issues. Previous attempts using spinner triggers, helper triggers, and timed magnet adjustments all failed due to inconsistent ball speeds. This event-driven approach is more reliable than any timing-based method.

Reducing Ball Angular Momentum on Magnet

When a ball is caught by a magnet, divide all angular momentum components (AngMomX, AngMomY, AngMomZ) by 3 on the timer that runs during magnet hold. This stops the visible ball spin that looks unrealistic.

Sword Lock Timing Workaround

The sword lock mechanism relies on a solenoid dropping and raising a post within ~270ms. PinMAME timing inconsistency causes the post to sometimes take up to 350ms, releasing two balls instead of one. The workaround uses a VPX timer to guarantee the post raises within 280ms maximum, regardless of PinMAME timing. This is the first VPX LOTR implementation using physics-based sword locking rather than the cvpmVLock class.

Sword Glow Effect

The glowing Sting sword uses 6 separate primitives: sword handle, sword letters, sword blade, sword blade overlay, a flasher above the sword, and a sideblade flasher. The glow overlay primitive is toggled invisible when the lamp level is 0. Glow is tied to ball lock events -- glowing when balls are locked on the ramp, pulsating when 2 balls are locked.

Sauron Eye Tracking

The Sauron eye toy animates to track ball position. The refined version: only tracks a limited set of switches in the lower playfield, only activates when the eye is "lit" per game state, and never looks away from the player position. Creates a subtle, unsettling effect.

Glow Ball Implementation

Glow balls are VPX lights that follow ball position. Ball image must be set BEFORE balls are created -- once created, the ball image cannot be changed. The glow light color CAN be changed in real time. LOTR explored mode-specific colors (red for DTR, orange for Bash Balrog, green for Shelob). Offered as a toggleable script option.

Flasher Timer and Decay Tuning

Flasher fade timers at 10ms with 0.9 decay coefficient create many rendering steps. Increasing to 20ms/0.8 significantly reduces fade steps and improves multiball performance. The ObjLevel exponent controls fade curve shape: 1 is linear, >1 is faster decay.

Ball Position Caching

When multiple script systems reference ball.x or ball.y, read the position value once into a local variable and reuse throughout the loop. Reading directly from the ball object each time is slower. Consider baking position caching into the cortracker class.

Ball Brightness Tied to GI

Ball brightness can be adjusted dynamically using Ball.color, making the ball appear dimmer when GI is off. Can be set during ball creation or dynamically in the rolling update routine.

UseVPMModSol

Enabling UseVPMModSol allows solenoids to receive modulated values from PinMAME instead of binary on/off. On Whitestar games like LOTR, it may not affect lamp signals. Test thoroughly as it can cause unexpected behavior.

PWM Insert Lighting

PWM support from VPinMAME 3.6 with UseVPMModSol=2: insert values range 0-255 but real hardware max output is ~170. GI callback outputs 0 to 1.0 (decimal), max ~0.75 for Whitestar. With PWM, Lampz fading code can be "neutered" -- fading handled by VPinMAME. Tables using PWM are NOT compatible with earlier PinMAME versions.

SetLocale for Regional Compatibility

VBScript SetLocale header can fix calculation errors on non-US regional settings. Should be standard practice for all tables.

Blacklight Mod

A blacklight effect can be achieved by selecting UV-reactive colors from the playfield art, creating a playfield-sized flasher with just those colors, and overlaying it above the playfield during specific game modes.

3D & Art

3D Scanning Workflow

Real LOTR pinball figures (Gandalf, Balrog, Cave Troll, Black Horse rider) were 3D scanned by Dazz. Original scans were extremely high poly (Gandalf at 1.1 million polys). These needed massive reduction for VPX use.

Poly Reduction Pipeline

Multiple approaches tested for reducing scan poly counts:

  1. Blender Decimate (Collapse mode): Simple but crude. Use ratio ~0.1 as first pass.
  2. Blender Remesh (Voxel): Better for complex models. Use voxel size ~0.5.
  3. Combined approach (recommended): Decimate to 0.1, then Remesh to 0.5, then another Decimate to 0.025. Got Gandalf from 1.1M to 5-8K triangles with baked texture.

Key insight from flupper1: "The texture will contain all the fine details" -- aggressive poly reduction is fine when paired with proper texture baking. Target 5-8K per figure (JD entire table is 275K tris, so 100K per figure is too much).

Texture Baking from High-Poly to Low-Poly

High-to-low poly bake workflow: create low-poly version via decimate/remesh, UV unwrap, bake texture from high-poly to low-poly using Blender Cycles. Common issue: wrong UV map selection causes garbled bakes.

UV Map Rewrapping

Complex figurines were manually UV-unwrapped for optimal texture quality. Original UV maps showed visible triangulation edges in VPX. Rewrapping eliminates artifacts. Labor-intensive but the investment carries forward to VPE.

Octane vs Cycles Rendering

LOTR-VE was rendered in Octane (not Cycles). Converting Octane-rendered tables to Toolkit is a "big job." The choice of HDRI environment map significantly affects the final look. Cast shadows from HDRI are a concern -- rotating the HDRI can minimize unwanted directional shadows.

Baked Texture Light Color Fix

Red GI bulbs reflected as orange/yellow in baked textures. Root cause: Octane's black body emission node produces warm-toned light regardless of diffuse color. Fix from Flupper: use a texture emission node instead, connecting a red texture to the pin. Alternatively, lower Kelvin temperature toward 1000K for redder emission.

GI ON/OFF Baking

Baking textures requires two passes: one with GI lights ON and one OFF. ROM-driven light sets (inserts, flashers) should NOT be baked -- they are controlled dynamically in VPX.

Blender Bulb Positioning

VPX bulb meshes don't export to Blender OBJ files automatically -- manually add simple primitives (elongated spheres) positioned at each bulb location. These serve as emissive light sources in Blender for GI bakes only.

Flasher Projection Renders

Flasher projections onto sideblades and playfield were rendered in Blender. Technique from Blood Machines: position camera at POV location, remove sideblade image, enable only the flasher lamps, render. Produces near-black-and-white textures for flasher shadow overlays.

Clear Plastic Transparency

For transparent/clear plastics: avoid using alpha transparency (causes holes in VR, wrong draw order). Instead, use a non-transparent texture with near-black background (#111111, not pure black). Set BlendDisableLighting = 2 for clear plastics.

Insert Text Methods

Insert text can be displayed using either a ramp or flasher over the playfield. The ramp method allows a single VPX light to illuminate both text and provide ball reflections. For proper ball reflections with either method, add a second VPX bulb at height -3 with 0 transmit, intensity 5, ball reflection enabled, falloff power 3.

Dynamic Refraction Effect

To simulate refraction through a transparent object (like the sword ramp): render 20-30 refraction images at incremental ball positions in Blender, then script texture swaps on a "shadow primitive" based on ball Y position. Use two overlapping primitives with alternating frames, fading each in/out to smooth visual choppiness. VPX 10.8 later added native refraction probes.

Material Opacity for Light Transmit

Setting material opacity to exactly 0.999999 (six nines) gives maximum control over light transmit through bulb values for plastic primitives. Ensure material type is "plastic with an image."

Movable Primitive Origins

For animated primitives that rotate (Balrog, right tower, lock post, diverter), the origin point must be set precisely at the hinge/rotation axis. If the UV map is modified, re-verify origin placement.

Plastic Scanning

Scan plastics at 600 DPI. Save as TIFF, not PDF (PDF often contains heavily compressed JPEGs). Press down on bowed used plastics for best results.

VR Rooms from 360 Panoramas

VR rooms use a sphere primitive with a panoramic image mapped to the inside surface. Sources for panoramic images: NVIDIA Ansel tool, Microsoft ICE (stitches photos), or pre-made panoramic images. WebP format allows massive VR room images (16500x6500 at only 1.7MB). DGrimmReaper added a Minas Tirith 360-degree sphere from ArtStation as a third room option.

Playfield Artifact Detection

To find tiny artifacts and stray pixels in playfield cutouts, enable a bright stroke effect (like red) on the layer in Photoshop. Imperfections "light up like a Christmas tree" against the stroke color.

Spinner Reflection Removal

A baked metal plate texture included a static reflection of the spinner. Since the spinner animates in VPX, the static reflection looked wrong. Re-bake with the spinner hidden. Moving objects should generally be excluded from bake passes.

Troubleshooting

Flasher Bloom Performance

Flasher blooms (transparent primitive overlays) are extremely GPU-expensive. When multiple flashers fire during multiball, FPS drops significantly. Solution: use a single shared bloom flasher primitive that gets rotated and assigned the correct bloom image when any flasher fires. The bloom follows the most recently activated flasher. This dramatically reduces concurrent transparent element count.

VUK FPS Drops

A ball sitting in a VUK can cause catastrophic FPS drops (from 60 to 6 FPS) when the VUK wire arc primitive is high-poly and collidable. Fix: create low-poly versions for collision while keeping high-poly visual versions non-collidable.

Playfield Mesh and VUK Interaction

Adding a playfield mesh with beveled holes near VUK kickers can cause ball levitation. Ensure kicker size is >18, adjust hit height to match bevel depth. To test, rename playfield_mesh to playfield_x to disable it.

Bulb Meshes Dark When Lit

If bulb meshes appear dark when the associated light is on, uncheck "Static Rendering" (Static Mesh) on the bulb primitive.

Skitso-Style Insert Artifacts

Skitso-style inserts REQUIRE the VPX "Reflect Dynamic Elements" video option to be enabled, or dark artifacts appear. This setting cannot be forced from within the table script. For large inserts where artifacts are too visible, consider reverting to standard inserts.

Insert Artifact from Size Alignment

After image optimization, insert artifacts appeared. Root cause: insert primitive edge exactly aligned with alpha cut in playfield texture. Fix: slightly increase insert primitive size to create overlap, and drop prims below playfield by 0.1 units.

VPX 10.7 Texture Deletion Bug

Random texture deletion/white textures on VPX 10.7. Cannot be reliably reproduced. Restarting VPX often fixes it. Issue NOT seen on 10.6. Decision: ship 10.6 version as primary release.

VPX 32-bit Memory Crash (2-Hour Limit)

LOTR stutters and crashes after ~2 hours of continuous play. Sound effects drop out first, then crash ~20 seconds later. Fix: 64-bit VPX -- perfectly smooth after 2+ hours. Requires 64-bit VPinMAME DLLs and Setup64.exe. Caveat: B2S server is 32-bit only, so no backglass in 64-bit mode.

VPX File Size Limits

Tables approaching 400-500MB experience texture loading failures. Contributing factors: 8K textures, total uncompressed texture memory, possibly VPX memory management. Removing a single 8K image could fix the issue. WebP helps file size but doesn't necessarily reduce GPU memory usage.

Ambient Light Inconsistency Between Bakes

Ambient light reflections appeared brighter in GI-OFF textures. This is a camera exposure artifact. Rather than fixing in Blender, manually edit the PNG texture to remove inconsistent glare.

Slingshot Animation Brightness Mismatch

Sling rubbers at rest are baked textures, but animated slings switch to VPX rubber objects that are slightly brighter. Fix: adjust animated sling rubbers to not overlap with baked sling texture edges.

Ramp Post Physics Alignment

Physical primitive posts were all shifted slightly -- someone had moved one post with the entire collection selected. The SwordRoof wall needed "bottom collidable" set to prevent ball escape.

Playfield Hole Reflections

Cutting a hole in the VPX playfield mesh does not eliminate reflections. Disable reflections on specific objects causing unwanted reflection near holes.

Depth Bias and Glow Punch-Through

When a glowing primitive has very negative depth bias (e.g., -1000), it can "punch through" other primitives. Fix: adjust depth bias to a less extreme value. Transparency rendering issues are worse with AA disabled.

Game Knowledge

Flipper Strength Calibration

LOTR flipper strength was calibrated by testing whether the left ramp (Legolas) could be backhanded from a cradle. Real machine owners confirmed backhanding from cradle is NOT normally possible. Final flipper strength settled at 3150 with rubber at 3.

Real Machine Comparison

Ring shot rejects more easily in real life than VPX (VPW intentionally made it easier). Palantir standup target is super bouncy on real machines, causing airballs. "Arwen protector" aftermarket mod redirects ball from ramp, breaking intended gameplay. Players confirmed VPX table "plays just like the real thing."

Texture Size Recommendations

Maximum recommended playfield/plastics texture: 2048x4096 (power of 2). Figurine textures reduced from 2048x2048 to 512x512 with no visible difference, saving 90MB. Always close VPX completely after exiting (memory not fully released). Enable texture compression for lower-end systems.

VPX Version Decision Matrix

  • 10.6: More stable, no texture bugs, WAV audio only
  • 10.7: WebP/OGG support, but texture deletion bug and higher memory usage
  • 10.8: Fixed DMD flickering, refraction probes, PWM support

LOTR v1.0 was 10.7 only, but v1.01+ shipped 10.6 due to bugs.

Resources

  • Photoshop stroke trick for finding playfield image artifacts around insert holes
  • NVIDIA Ansel for VR room 360-degree panorama capture
  • Microsoft ICE for stitching multiple photos into panoramas
  • chaiNNer tool for lossless WebP conversion
  • "Blender how to Reduce Poly Count and Bake Textures" YouTube tutorial
  • VPX Collection Manager for batch primitive operations
  • B2S backglass custom artwork pipeline by HauntFreaks
  • Sketchfab for 3D model references