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

211 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Portal VR Media Viewer
Godot 4.6 Forward Plus / Mobile renderer project. Flat file structure -- all files in project root.
See [README.md](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.godot) | Project config: Forward Plus renderer, Jolt Physics, OpenXR, mobile renderer, feature flags |
| [game_loader.gd](game_loader.gd) | Single script: loads `portal.tscn` at startup |
| [portal.gd](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](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](tile.gd) | Individual video tile: `place_on_shell()`, `set_global_position_unclamped()`, billboard to camera, pulse scale animation. `class_name Tile` |
| [cloud_1.gdshader](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](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](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.tscn) | Tile scene: PlaneMesh (1.6x0.9) with StandardMaterial3D, AnimationPlayer with "flying" loop (3.2s) |
| [emission_point.tscn](emission_point.tscn) | EmissionPoint node + Timer (1.67s) |
| [boot.tscn](boot.tscn) | Entry scene: Node2D with game_loader.gd |
| [export_presets.cfg](export_presets.cfg) | 48 presets: 24 Windows + 24 Linux + Android variants. Each has unique `custom_features` + `include_filter` |
| [openxr_action_map.tres](openxr_action_map.tres) | OpenXR actions for left/right hands (trigger, grip, trigger_click, trigger_touch, etc.) |
| [animations.res](animations.res) | Animation library for prompt overlays |
| [convert.sh](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](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).