Files
2026-06-17 15:28:56 -07:00

12 KiB
Raw Permalink Blame History

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:

  1. Phase 1 (0 → 0.5): Positions lerp from start → target via smoothstep()
  2. Phase 2 (0.5 → 1.0): Scale multiplier fades from 0 → 1 (tiles pop in)
  3. 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) + accumulator on 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 (always boot.tscn, tile.tscn, emission_point.tscn; main portal varies).
  • export_path: output binary path in bin/.

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 uses AudioStreamMP3.
  • Video content uses .ogv format, not bundled in git (filtered by include_filter).
  • Clear color is transparent (Color(0,0,0,0)) -- not black.
  • animations.res contains 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).