A multiplayer Virtual Reality art creation and exhibition platform built in Unity.
Artists can paint in 3D space, collaborate in real time, submit works to galleries, and visit exhibitions from anywhere.
- Clone the repository from GitHub.
- Open Unity Hub and select Add / Add project from disk.
- Choose the Unity project folder:
code/VR Art Gallery/. - If Unity Hub prompts for an editor install, install Unity
6000.2.2f1and open the project with that version. - Let Unity finish importing packages and assets. The package set for this project is defined in
code/VR Art Gallery/Packages/manifest.json. - To build/deploy the project to a VR device, open File -> Build Profiles (or Build Settings), confirm the target platform, and build using the enabled scenes already listed in
ProjectSettings/EditorBuildSettings.asset. - To deploy:
- Windows PC: switch to Windows, then run Build.
- Meta Quest / Horizon OS: switch to Android, connect the headset with developer mode enabled, then run Build And Run.
Unity editor version note: the exact project editor version is
6000.2.2f1incode/VR Art Gallery/ProjectSettings/ProjectVersion.txt. The package manifest incode/VR Art Gallery/Packages/manifest.jsonmatches this Unity 6 setup.
The system is structured across three principal layers: a Unity VR Client running on Meta Horizon OS or Windows PC, a Backend Services layer (Supabase, Netcode for GameObjects), and a Cloud/Storage layer for persisted artwork and user data.
- Real-time collaborative painting — Multiple users can paint on the same canvas simultaneously using VR controllers; strokes are synchronised via
CanvasStrokeSyncNgo(Netcode Server/Client RPCs). - Gallery creation & curation — Authenticated artists can create named galleries, submit artwork, and manage submissions.
- Role-based access — Three user tiers (
Guest,Artist,Admin) enforced throughUserRoleand Supabase Row-Level Security. - Cloud artwork persistence — Paintings are saved as PNG files, uploaded to Supabase Storage with auto-generated thumbnails, and indexed in the
artworkPostgreSQL table. - Ambient spatial audio & audiobook playback — Spatialized soundscapes and chapter-selectable audiobooks with bookmarking.
- ACL-based collaboration — Artwork owners can grant collaborator access tracked in the
acl_artworktable and enforced bySupabaseArtworkAccessService.CanCurrentUserJoinArtworkAsync. - Automated tests — Edit Mode and Play Mode Unity test suites run automatically on every push via the Test workflow.
Project_X/
├── code/
│ └── VR Art Gallery/ # Unity project root
│ └── Assets/
│ └── Scripts/
│ ├── Authentication/ # AuthenticationManager, UserProfile, UserRole
│ ├── Cloud/
│ │ ├── API/ # SupabaseArtworkRepository, SupabaseGalleryRepository,
│ │ │ # SupabaseArtistRepository, SupabaseAuthService,
│ │ │ # SupabaseArtworkAccessService, IGalleryRepository,
│ │ │ # IArtworkRepository, IArtistRepository
│ │ ├── Database/ # SupabaseClient singleton
│ │ └── Models/ # ArtworkData, GalleryData, ArtistProfile, AclArtworkEntry
│ ├── Core/ # GameManager (singleton, scene routing)
│ ├── Logging/ # CloudLogger, ICloudCodeClient, UnityCloudCodeClient
│ ├── Network/ # CanvasStrokeSyncNgo (NGO RPCs), NgoArtworkJoinGate
│ ├── UI/ # AuthenticationUIController, UserStatusDisplay
│ └── Testing/
│ ├── EditMode/
│ │ ├── Cloud/
│ │ │ ├── Artist/ # ArtistRepositoryTest
│ │ │ ├── Artwork/ # ArtworkRepositoryTests, MockArtworkTests,
│ │ │ │ # SupabaseStorageIntegrationTests
│ │ │ └── Gallery/ # GalleryRepositoryTestAndPopulate
│ │ └── AnG/ # SaveArtTest, LoadArtTest
│ └── PlayMode/ # Integration tests
│ # Root-level brush scripts:
│ # BrushToolState, BrushGrabState, BrushRespawnOnGrab,
│ # BrushVrColorCycleInput, BucketColorPaletteOpener, CanvasBrushSpawner
├── backend/
│ └── SupabaseAuth/ # Standalone C# console app for backend auth testing
├── uml/
│ ├── components/ # Component diagrams (Simplified & Complete)
│ ├── sequence diagrams/ # PlantUML source + rendered PNGs per use case
│ └── technical_architecture/ # Full tech-stack architecture diagram
├── docs/
│ └── screenshots/ # Gallery screenshots (add yours here)
└── .github/
└── workflows/
└── run-tests.yaml # CI: Unity Test Runner (edit + play mode)
Tests run automatically via GitHub Actions (.github/workflows/run-tests.yaml) on every push or pull request.
- Frees CI disk space — prunes Docker and system packages to fit the Unity install.
- Checks out the repo with LFS — ensures binary assets are present.
- Strips incompatible Meta packages — replaces
com.meta.xr.sdk.allwith the headless-compatiblecom.meta.xr.sdk.coreso the Linux runner can compile the project. - Injects Supabase credentials — writes
SUPABASE_URLandSUPABASE_KEYfrom GitHub Secrets into a.envfile consumed at runtime. - Runs Unity Test Runner (
game-ci/unity-test-runner@v4) — executes botheditmodeandplaymodeassemblies (PlayMode,EditMode) in parallel matrix jobs. - Deletes the
.env— secrets are always cleaned up, even on failure. - Uploads artifacts — test results (XML) and coverage reports (HTML + badge) are uploaded per test mode.
| Assembly | Tests |
|---|---|
EditMode — Auth UI |
AuthenticationUI_EditModeTests — panel visibility, field clearing, input validation, error display |
EditMode — Cloud logging |
CloudLoggerTests — cloud logging correctness |
EditMode — Auth service |
SupabaseAuthTest — sign-up, sign-in, sign-out, session refresh flows |
EditMode — Artwork CRUD |
SupabaseArtworkTest, ArtworkRepositoryTests, MockArtworkTests, SupabaseStorageIntegrationTests — artwork creation, storage upload/download |
EditMode — Gallery CRUD |
SupabaseGalleryTest, GalleryRepositoryTestAndPopulate, SupabaseArtworkGalleryTest — gallery creation, artwork assignment, slot mapping |
EditMode — Artist |
ArtistRepositoryTest — artist profile creation and retrieval |
EditMode — Full workflow |
FullWorkflowTest — end-to-end: sign-in → create artwork → upload → submit to gallery |
EditMode — Invites |
InviteTest — ACL collaboration invite flow |
EditMode — Art I/O |
SaveArtTest, LoadArtTest — canvas PNG save and reload |
PlayMode |
Sample integration tests; extended as features ship |
The following is a concrete end-to-end example of what a user can do in the VR environment.
The user launches the application on their VR headset or PC. An in-headset UI panel appears.
- New user — taps Register in the
AuthenticationUIControllerpanel, fills in email, username, and password. TheAuthenticationManager.RegisterUsermethod callsSupabaseClientInstance.Auth.SignUp, thenSupabaseArtistRepository.CreateArtistProfileAsyncto write the user's profile to theartiststable. The user is assigned theArtistrole. - Returning user — taps Login;
AuthenticationManager.LoginUsercallsSupabaseClientInstance.Auth.SignInand firesOnUserLoggedIn, which updates theUserStatusDisplayHUD overlay with the artist's username and role.
In the workspace the user faces a paintable quad. Physical brush props are spawned by CanvasBrushSpawner; grabbing one triggers BrushGrabState, which attaches the brush to the controller and registers it with the canvas.
- The
PaintableSurfaceRTcomponent initialises two ping-pongRenderTexturebuffers (_a,_b) at 1024 × 1024. BrushToolStateholds the active paint settings (color, radius, hardness) and exposesCurrentBrushStateto the rest of the system. Color can be changed viaSetColor,NextPresetColor/PreviousPresetColor, or theBrushVrColorCycleInputcomponent which maps controller button presses to color cycling. TheBucketColorPaletteOpenerspawns a hand-held palette for freeform color selection.- The user squeezes the right controller trigger —
XRPainterRayInputdetects the press viaIsDrawing(), casts a physics ray, and resolves the hit UV coordinate fromTryGetStrokeTarget. BeginStrokecallsCanvasStrokeSyncNgo.LocalStrokeBegin, which registers the stroke locally and fires aStrokeBeginServerRpcso every other connected client is notified.- As the controller moves,
AddStrokeSampleinterpolates intermediate UV points and batches them. When the batch reachesflushPointThreshold,FlushBatchcallsCanvasStrokeSyncNgo.LocalStrokePoints→StrokePointsServerRpc→StrokePointsClientRpcto replicate the stroke on all peers. - Each
PaintAt(uv, brush)call GPU-blits the brush mask onto the currentRenderTexturebuffer. - Releasing the trigger calls
EndStroke→CanvasStrokeSyncNgo.LocalStrokeEnd→StrokeEndServerRpc.
Autosave: Every 10 seconds (configurable in the Inspector), PaintableSurfaceRT.SaveCanvasToPNG writes a timestamped PNG to the device's Application.persistentDataPath/Paintings/ directory.
Canvas modes are controlled via PaintableSurfaceRT.SetMode: DrawOnBlank starts from a clear canvas, DrawOverImage seeds the canvas from an existing texture, and DisplayOnly locks it for viewing.
A second artist can join the same canvas session. NgoArtworkJoinGate.CanCurrentUserJoinArtworkAsync checks the acl_artwork table to confirm the requesting artist either owns the artwork or holds an active ACL entry. Once granted, both artists paint and see each other's strokes in real time through the NGO RPC pipeline described above.
When the painting is ready:
- The user opens the HUD and selects Submit Artwork.
SupabaseArtworkRepository.CreateArtworkWithUploadAsyncis called with the PNG byte array. It:- Generates a half-size thumbnail via
GenerateHalfSizePng. - Uploads both images to the
artwork-imagesSupabase Storage bucket at paths scoped to the artist'sowner_id. - Inserts an
ArtworkDatarecord into theartworkPostgreSQL table (image_url,thumbnail_url,filesize_bytes, timestamps).
- Generates a half-size thumbnail via
- Available galleries are fetched via
SupabaseGalleryRepository.GetAllGalleriesAsyncand displayed in the HUD. The user selects a target gallery. SupabaseGalleryRepository.AddArtworkToGalleryAsyncadds the artwork ID to the gallery'sartwork_idslist and persists the updatedGalleryDatarecord. Gallery owners can then usePlaceArtworkInSlotto assign the piece to a specific display position in the physical gallery space.
Any user (including guests) can browse public exhibitions. GameManager.LoadGallery checks CanAccessGallery() and loads the Gallery scene. Artwork is loaded from Supabase Storage using time-limited signed URLs generated by SupabaseArtworkRepository.CreateSignedUrlAsync and downloaded with DownloadWithSignedUrlAsync. The PaintingDisplayLocal component renders each artwork on a display quad inside the gallery.
All sequence diagrams were authored in PlantUML. The .txt source files live alongside their rendered .png exports under uml/sequence diagrams/.
| Use Case | Diagram |
|---|---|
| Sign Up / Sign In | uml/sequence diagrams/signup/ · uml/sequence diagrams/Sign_In/ |
| Create 3D Art | uml/sequence diagrams/create-3d-art/ |
| Create GenAI Art | uml/sequence diagrams/Create_GenAI_Art/ |
| Work on Artwork | uml/sequence diagrams/work on artwork/ |
| Submit an Art Piece | uml/sequence diagrams/submit_an_art_piece/ |
| Create an Art Gallery | uml/sequence diagrams/create_an_art_gallery/ |
| Browse / Join Gallery | uml/sequence diagrams/browse_and_join_gallery/ |
| Customise Gallery | uml/sequence diagrams/customise_gallery/ |
| Customise Workspace | uml/sequence diagrams/customiseWorkspace/ |
| Interact with an Art Piece | uml/sequence diagrams/interact_with_an_art_piece/ |
| Collaborate | uml/sequence diagrams/collaborate/ |
| Communicate | uml/sequence diagrams/communicate/ |
| Review Submission Request | uml/sequence diagrams/review_submission_req/ |
| Delete Account | uml/sequence diagrams/delete_account/ |
| Sign Off | uml/sequence diagrams/signoff/ |
master— stable, tagged releases onlydevelopment— integration branch; all feature branches merge here
Tests must pass (Edit Mode and Play Mode) before a PR to either branch can be merged. The GitHub Actions workflow enforces this automatically.
Unity serialises scenes (.unity), prefabs (.prefab), assets (.asset), and materials (.mat) as YAML text files. When two branches modify the same scene or prefab, a standard line-based merge will almost always produce a corrupt file. UnityYAMLMerge is Unity's smart merge tool that understands this format and resolves conflicts safely.
The repository's .gitattributes (inside code/VR Art Gallery/) already declares the merge driver for the relevant file types:
*.unity merge=unityyamlmerge
*.prefab merge=unityyamlmerge
*.asset merge=unityyamlmerge
*.mat merge=unityyamlmerge
This tells git which driver to call, but each developer must register that driver in their local git configuration. Run the following commands once after cloning (adjust the path to match your Unity installation):
Windows
git config --global merge.unityyamlmerge.name "UnityYAMLMerge"
git config --global merge.unityyamlmerge.driver "C:\Program Files\Unity\Hub\Editor\<version>\Editor\Data\Tools\UnityYAMLMerge.exe merge -p %O %B %A %A"
macOS
git config --global merge.unityyamlmerge.name "UnityYAMLMerge"
git config --global merge.unityyamlmerge.driver "/Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/Tools/UnityYAMLMerge merge -p %O %B %A %A"
Replace <version> with 6000.2.2f1, the Unity editor version used by this project.
Once configured, git will automatically invoke UnityYAMLMerge whenever a conflict is detected in a tracked Unity file. If UnityYAMLMerge cannot resolve a conflict automatically it falls back to a standard three-way diff, which can then be resolved manually.
The following secrets must be configured in GitHub Actions (Settings → Secrets and variables → Actions) and in a local .env file at the project root for local backend testing:
SUPABASE_URL=https://<your-project>.supabase.co
SUPABASE_KEY=<your-anon-or-service-role-key>
Never commit
.envto version control. It is listed in.gitignore.


