12 KiB
Portal VR Media Viewer
Godot 4.6 Forward Plus / Mobile renderer project. Flat file structure -- all files in project root. See README.md for human-facing overview.
Architecture
boot.tscn (Node2D, game_loader.gd)
|
+-- portal.tscn (Node3D, portal.gd) <-- main scene
|
+-- XROrigin3D (XR tracking root)
| +-- Perspective (XRCamera3D) <-- viewer camera
| +-- AudioStreamPlayer (music)
| +-- MeshInstance3D (outer tunnel, r=2.0)
| +-- MeshInstance3D2 (mid tunnel, r=1.5)
| +-- MeshInstance3D3 (inner tunnel, r=1.0)
| +-- Blurrycap (sphere cap shader)
| +-- prompt_1 / prompt_2 timers
|
+-- EmissionPoint (emission_point.tscn)
|
+-- SubViewport (shared render target)
+-- VideoStreamPlayer (shared video)
+-- tile.tscn instances (presentation layout)
Boot -> Portal -> EmissionPoint -> Tile shell is the entry flow. EmissionPoint creates a configurable tile layout (hemisphere, wraparound, 9-patch, full sphere, ring of rings, spiral staircase) of tile instances, each showing the shared video via a SubViewport texture.
Key Files
| File | Role |
|---|---|
| project.godot | Project config: Forward Plus renderer, Jolt Physics, OpenXR, mobile renderer, feature flags |
| game_loader.gd | Single script: loads portal.tscn at startup |
| portal.gd | Main scene controller: tunnel shader params, XR init, gamepad/mouse look, auto-centering, prompt timers, tunnel color/spiral export vars |
| emission_point.gd | Core innovation: builds tile layouts from sphere face directions, shared video pool, pulse animation, format switching. Key exports: slot_limit, base_radius, pulse_amplitude, pulse_speed_hz, hemisphere_coverage_degrees, sphere_rings, sphere_segments, deterministic_seed, presentation_format (enum), per-format params (wraparound_radius, nine_patch_radius, full_sphere_radius, ring_of_rings_*, spiral_staircase_*, transition_duration_sec) |
| tile.gd | Individual video tile: place_on_shell(), set_global_position_unclamped(), billboard to camera, pulse scale animation. class_name Tile |
| cloud_1.gdshader | Tunnel shader: cloud + spiral helix on CylinderMesh. Parameters: movement_Factor, rotation_speed, scroll_speed, vertex_displacement, inner_fade, outer_fade, color, spiral_color, spiral_turns, spiral_swirl, spiral_width, spiral_strands. Inputs: noise_tex, cloud_tex (NoiseTexture2D) |
| cap.gdshader | Blurrycap shader: screen-space distortion with Fresnel swirl + fog. Parameters: distortion_strength, swirl_strength, noise_amount, fog_density, tint_color. Uses SCREEN_UV + hint_screen_texture for world-behind-sphere effect |
| portal.tscn | Main scene: 3 CylinderMesh (radii 2.0/1.5/1.0, each 10 tall), 3 tunnel materials, Blurrycap sphere, EmissionPoint child (base_radius=9.0, seed=0), prompt planes, timers (45s/70s), audio player |
| tile.tscn | Tile scene: PlaneMesh (1.6x0.9) with StandardMaterial3D, AnimationPlayer with "flying" loop (3.2s) |
| emission_point.tscn | EmissionPoint node + Timer (1.67s) |
| boot.tscn | Entry scene: Node2D with game_loader.gd |
| export_presets.cfg | 48 presets: 24 Windows + 24 Linux + Android variants. Each has unique custom_features + include_filter |
| openxr_action_map.tres | OpenXR actions for left/right hands (trigger, grip, trigger_click, trigger_touch, etc.) |
| animations.res | Animation library for prompt overlays |
| convert.sh | ffmpeg utility: ffmpeg -i "$1" -vf "scale=-1:420" -q:v 2 -q:a 1 -g:v 50 "$1".ogv |
Shader Reference
cloud_1.gdshader (spatial, unshaded, cull_disabled)
Inputs: noise_tex (NoiseTexture2D), cloud_tex (NoiseTexture2D)
Both must be NoiseTexture2D with FastNoiseLite -- set in scene file.
| Parameter | Purpose |
|---|---|
movement_Factor |
How fast the cloud pattern drifts |
rotation_speed |
Rotation of the tunnel texture |
scroll_speed |
UV scrolling speed |
vertex_displacement |
Vertex displacement intensity |
inner_fade |
Fade at tunnel inner radius |
outer_fade |
Fade at tunnel outer radius |
color |
Base tunnel color |
spiral_color |
Spiral overlay color |
spiral_turns |
Number of spiral rotations |
spiral_swirl |
Tightness of spiral curl |
spiral_width |
Width of spiral arms |
spiral_strands |
Number of spiral arms (1 or 2 for double helix) |
cap.gdshader (spatial, unshaded, cull_disabled)
Uses: SCREEN_UV + hint_screen_texture for world-behind-the-sphere effect.
| Parameter | Purpose |
|---|---|
distortion_strength |
How much the screen is warped |
swirl_strength |
Swirl distortion intensity |
noise_amount |
Procedural noise added to distortion |
fog_density |
Fog depth for the blurred look |
tint_color |
Color tint of the sphere |
Code Patterns
Tunnel parameter propagation
portal.gd has @export Color/float properties for each of 3 tunnels. Setters call _set_mesh_shader_parameter() on CylinderMesh.material -> ShaderMaterial. Editor sync via _process() + _enter_tree().
Shared video pool
EmissionPoint creates ONE SubViewport + ONE VideoStreamPlayer. All tiles share the viewport texture -- no per-tile video players.
Hemisphere layout
_build_hemisphere_slots() samples sphere mesh face centers (via sphere_rings x sphere_segments), filters by view direction dot product (hemisphere_coverage_degrees), downsamples to slot_limit. Uses deterministic_seed for reproducible layout.
Tile pulse
Sine wave in _process() on mesh.scale, synced via phase_offset per tile.
Billboard
Tiles look_at(camera.global_position) every frame. inward_face_flip rotates PI on Y to show the correct face.
Auto-centering
After 1.5s of no input, perspective rotates back to zero at 0.05 rad/s.
Editor mode guard
Engine.is_editor_hint() checks throughout portal.gd to prevent runtime errors in Godot editor.
@tool on portal.gd
Enables editor property sync for all tunnel shader parameters while editing.
Presentation Formats
EmissionPoint supports 6 presentation layouts, switchable at runtime via presentation_format enum (@export). Tiles are never created or destroyed on format switch — they are repositioned via a smooth two-phase transition (position lerp + scale fade) over transition_duration_sec seconds.
Available Formats
| Format | Layout | Animation |
|---|---|---|
HEMISPHERE |
Hemisphere of directions facing camera (existing) | Sinusoidal radius pulse |
WRAPAROUND |
Horizontal circle at camera height around viewer | Circle rotates at wraparound_rotation_speed_rad_s |
NINE_PATCH |
Fixed 3x3 grid in camera-local space | Per-tile scale wobble via nine_patch_wobble_* params |
FULL_SPHERE |
Uniform sphere via Fibonacci golden-angle distribution | Static positions, only pulse (no rotation) |
RING_OF_RINGS |
3 concentric rings (N/4 inner, N/2 middle, N/4 outer) | Each ring rotates at independent speed → parallax |
SPIRAL_STAIRCASE |
Rising helix at fixed radius, climbing over spiral_staircase_turns rotations |
Slow spiral rotation + per-tile vertical drift; partial billboard |
Per-Format Parameters
Wraparound: wraparound_radius, wraparound_rotation_speed_rad_s
Nine-Patch: nine_patch_radius, nine_patch_wobble_speed_hz, nine_patch_wobble_amplitude, nine_patch_phase_spread
Full Sphere: full_sphere_radius, full_sphere_use_fibonacci
Ring of Rings: ring_of_rings_inner_radius, ring_of_rings_middle_radius, ring_of_rings_outer_radius, ring_of_rings_inner_speed, ring_of_rings_middle_speed, ring_of_rings_outer_speed, ring_of_rings_vertical_wobble
Spiral Staircase: spiral_staircase_radius, spiral_staircase_height, spiral_staircase_turns, spiral_staircase_rotation_speed_rad_s, spiral_staircase_wobble_amplitude
Common: transition_duration_sec — shared transition duration for all format switches
Transition System
The transition uses _handle_transition(time_sec) in emission_point.gd:
- Phase 1 (0 → 0.5): Positions lerp from start → target via
smoothstep() - Phase 2 (0.5 → 1.0): Scale multiplier fades from 0 → 1 (tiles pop in)
- On completion:
_current_format = _target_format,_format_transition_progress = 1.0
Extra slots in smaller layouts (e.g., 9-patch with 60 tile capacity) are placed at Vector3.INF and become visible during the fade-in.
Key Math
- Wraparound:
angle = (2π*i/N) + accumulatoron Y=0 plane - Nine-Patch:
center + cam_right*(col-1)*spacing + cam_up*(row-1)*spacing - cam_forward*radius - Full Sphere: Fibonacci golden angle
γ = π(3-√5),y = 1 - (2i+1)/N - Ring of Rings: N/4 inner ring, N/2 middle ring, rest outer — each with independent rotation
- Spiral Staircase:
angle = (i/N)*turns*τ + accumulator,height = (i/N - 0.5)*height
Setting via Export Preset
Presentation format can be governed externally by setting the presentation_format export on the EmissionPoint node. In timeline/content editor workflows, this parameter is bindable via Godot's property binding system — a server or external controller can call emission_point.set_presentation_format(EmissionPoint.PresentationFormat.NINE_PATCH) at any time.
Export Preset System
Content is controlled entirely through export_presets.cfg, not separate scene files. Each preset:
custom_features: comma-separated flags (e.g.,smoke,poppers,stoner_dad_3). Used at runtime for conditional logic.include_filter: glob for video files to bundle (e.g.,*chubs*.ogv,stoner_dad_3.ogv).export_files: which scene files to include (alwaysboot.tscn,tile.tscn,emission_point.tscn; main portal varies).export_path: output binary path inbin/.
Content variants
| Variant | Video Pattern |
|---|---|
| stoner_dad_2 | *stoner_dad_2*.ogv |
| stoner_dad_3 | *stoner_dad_3*.ogv |
| stoner_plastic | *stoner_plastic*.ogv |
| chubs_compilation | *chubs*.ogv |
| 420_special | *420_special*.ogv |
| 420_special_diet | *420_special_diet*.ogv |
| orc_domination | *orc*.ogv |
| furry_poppers_daddy | *poppers*.ogv |
| furry_poppers_stable_stud | *poppers*.ogv |
| toon_goon | *toon*.ogv |
| furry_goon_encouragement | *furry_goon*.ogv |
| furry_big_bad_wolf | *wolf*.ogv |
Platform outputs
- Windows:
bin/windows/portal_<name>.exe - Linux:
bin/linux/portal_<name>.x86_64 - Android (Meta Quest):
bin/android/portal_<name>.apk
Common Modifications
Change tunnel colors/spirals: Edit portal.tscn -> Node3D node properties or portal.gd defaults.
Change tile count/coverage: Edit emission_point.tscn -> EmissionPoint node -> slot_limit, hemisphere_coverage_degrees, sphere_rings, sphere_segments.
Add new content variant: Add export preset in Godot (Export -> Presets), set custom_features + include_filter + export_files + export_path.
Add prompt overlay: Add MeshInstance3D + Animation to portal.tscn, connect timer -> AnimationPlayer.play().
Change video: Place .ogv in project root (use convert.sh), set video_stream export on EmissionPoint, update include_filter in export preset.
Important Notes
- No subdirectories for source code. Everything is flat in project root.
- Audio files are
.mp3(not ogv). Music player usesAudioStreamMP3. - Video content uses
.ogvformat, not bundled in git (filtered byinclude_filter). - Clear color is transparent (
Color(0,0,0,0)) -- not black. animations.rescontains animation libraries for prompt overlays.- Boot scene hardcodes main scene as
"portal.tscn"string rather than UID -- a minor maintenance fragility. - Hand tracking enabled by default on Meta Quest (via custom Android manifest).