Implementation

Detailed explanation of how key features were implemented in the Portalt platform

Implementation Overview

Architecture Overview

The Portalt platform is implemented using a modern, scalable architecture that combines web technologies with Unity-based VR/AR capabilities. The system is built on a microservices architecture with clear separation of concerns between the admin dashboard, Unity applications, and backend services.

The architecture follows a layered approach, with each component handling specific responsibilities:

  • • Frontend Layer: Next.js-based admin dashboard for scene management and configuration
  • • Backend Layer: Node.js services handling data persistence and business logic
  • • Storage Layer: Hybrid storage system using SQLite for configurations and IBM Cloud Object Storage for assets
  • • Unity Layer: VR/AR applications consuming scene configurations and assets

Technology Stack

The platform leverages modern web technologies and cloud services to deliver a robust VR/AR experience:

Frontend Technologies
  • • Next.js for the admin dashboard
  • • React for UI components
  • • TypeScript for type safety
  • • Tailwind CSS for styling

The frontend stack is chosen for its developer experience, type safety, and component reusability.

Backend Technologies
  • • Node.js with Express
  • • SQLite for configuration storage
  • • IBM Cloud Object Storage for assets
  • • WebSocket for real-time updates

The backend stack prioritizes performance, scalability, and real-time capabilities.

Key Implementation Features

The platform implements several key features that work together to provide a seamless VR/AR experience:

Core Features
  • • VR environment creation and management
  • • Interactive 3D object placement
  • • User authentication and organization management
  • • Real-time scene synchronization
  • • Asset management and storage
Technical Features
  • • Type-safe API development
  • • Secure authentication flow
  • • Efficient asset delivery
  • • Real-time data synchronization
  • • Cross-platform compatibility

Development Approach

The implementation follows a modular, component-based approach with emphasis on:

  • • Clean code architecture and SOLID principles
  • • Comprehensive error handling and validation
  • • Performance optimization and caching strategies
  • • Security-first development practices
  • • Automated testing and continuous integration

This approach ensures maintainable code, reliable performance, and robust security.

Integration Points

The platform integrates with various services and systems to provide a complete solution:

External Services
  • • Clerk for authentication
  • • IBM Cloud Object Storage
  • • Unity VR/AR applications
  • • WebSocket server for real-time updates
Internal Systems
  • • Scene configuration management
  • • Asset processing pipeline
  • • User session management
  • • Organization-based access control

Performance Considerations

The implementation includes several performance optimizations to ensure smooth operation:

  • • Efficient asset loading and caching
  • • Optimized database queries
  • • Batch processing for multiple operations
  • • Progressive loading of 3D models
  • • WebSocket-based real-time updates

These optimizations are crucial for delivering a responsive VR/AR experience.

Security Measures

Security is implemented at multiple levels to protect user data and assets:

  • • Secure authentication and authorization
  • • Organization-based data isolation
  • • Secure asset access control
  • • API request validation
  • • Environment-based configuration

Testing Strategy

The implementation includes comprehensive testing to ensure reliability:

  • • Unit tests for core functionality
  • • Integration tests for API endpoints
  • • End-to-end testing of user flows
  • • Performance testing and optimization
  • • Security testing and validation

Future Considerations

The implementation is designed to support future enhancements:

  • • Scalable architecture for growth
  • • Modular design for feature additions
  • • Support for additional VR/AR features
  • • Enhanced collaboration capabilities
  • • Advanced analytics and reporting

Key Feature 1: VR Environment Creation

Overview

The application implements a comprehensive VR environment creation system that allows users to create, configure, and manage 3D scenes for VR activities. The system supports 3D object placement, scene configuration, and real-time synchronization with Unity applications.

Implementation Details

1. Scene Configuration System

1.1 Scene Data Structure
interface SceneConfiguration {
  _id?: string;
  activity_id: string;
  scene_id: string;
  objects: SceneObject[];
  orgId: string;
  createdAt: Date;
  updatedAt: Date;
}

interface SceneObject {
  object_id: string;
  modelUrl: string;
  position: Vector3;
  rotation: Vector3;
  scale: Vector3;
}

interface Vector3 {
  x: number;
  y: number;
  z: number;
}
1.2 Scene Editor Component
export function SceneEditor({ activity }: SceneEditorProps) {
  const [selectedScene, setSelectedScene] = useState<string | null>(null);
  const [sceneConfigs, setSceneConfigs] = useState<Record<string, any>>({});
  
  // Initialize scene for VR activities
  useEffect(() => {
    if (activity.format === 'VR' && activity.scenes?.[0]) {
      setSelectedScene(activity.scenes[0].id);
      setSceneConfigs(prev => ({
        ...prev,
        [activity.scenes[0].id]: activity.scenes[0].config
      }));
    }
  }, [activity]);
}

2. 3D Object Management

2.1 Object Addition
const handleAddArtifact = async (sceneId: string, artifact: any) => {
  const newObject = {
    object_id: crypto.randomUUID(),
    modelUrl: artifact.modelUrl,
    position: { x: 0, y: 0, z: 0 },
    rotation: { x: 0, y: 0, z: 0 },
    scale: { x: 1, y: 1, z: 1 }
  };

  const currentConfig = sceneConfigs[sceneId] || { objects: [] };
  const currentObjects = Array.isArray(currentConfig.objects) ? currentConfig.objects : [];

  await fetch(`/api/scenes-configuration/${sceneId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      objects: [...currentObjects, newObject]
    })
  });
};
2.2 Object Transformation
const handleUpdateArtifact = async (sceneId: string, objectId: string, updated: any) => {
  const updatedObjects = sceneConfigs[sceneId]?.objects?.map((obj: { object_id: string; }) => 
    obj.object_id === objectId ? { ...obj, ...updated } : obj
  ) || [];

  await fetch(`/api/scenes-configuration/${sceneId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      objects: updatedObjects
    })
  });
};

Scene Creation Flow

Scene Creation Flow Diagram

Unity Integration

1. Scene Configuration Access

Unity applications can access scene configurations through public API endpoints:

// Example Unity API call
const response = await fetch(`/api/scenes-configuration/${sceneId}?orgId=${orgId}`);
const config = await response.json();

2. 3D Model Access

Unity applications can access 3D models through public COS URLs:

// Example model URL structure
const modelUrl = `https://${bucketName}.s3.${region}.cloud-object-storage.appdomain.cloud/documents/${modelKey}`;

Scene Editor Features

1. Object Manipulation

  • • Position adjustment (x, y, z coordinates)
  • • Rotation control (x, y, z angles)
  • • Scale modification (x, y, z dimensions)
  • • Real-time preview

2. Asset Management

  • • 3D model upload
  • • Asset library integration
  • • Model format validation
  • • Automatic asset categorization

3. Scene Organization

  • • Multiple scene support
  • • Scene naming and ordering
  • • Scene duplication
  • • Scene deletion

Data Synchronization

1. Real-time Updates

  • • Scene configuration changes
  • • Object transformations
  • • Asset updates
  • • Unity app synchronization

2. Version Control

  • • Scene version tracking
  • • Change history
  • • Rollback capability
  • • Conflict resolution

Security Implementation

1. Access Control

  • • Organization-based access
  • • Scene-level permissions
  • • Asset access control
  • • API authentication

2. Data Validation

// Example validation
if (!format || !["AR", "VR"].includes(format)) {
  return res.status(400).json({ message: "Valid format (AR/VR) is required" });
}

Performance Optimization

1. Asset Loading

  • • Progressive loading
  • • Asset caching
  • • Lazy loading
  • • Format optimization

2. Scene Management

  • • Efficient scene switching
  • • Memory management
  • • Resource cleanup
  • • Batch operations

Error Handling

1. Validation Errors

try {
  const parseResult = ActivitySchema.safeParse(newActivity);
  if (!parseResult.success) {
    return res.status(400).json({ errors: parseResult.error.format() });
  }
} catch (error) {
  console.error("[ACTIVITIES_POST]", error);
  return res.status(500).json({ message: "Internal Error" });
}

2. Asset Errors

  • • File format validation
  • • Upload error handling
  • • URL validation
  • • Access error recovery

Testing Considerations

1. Scene Editor Tests

  • • Object manipulation
  • • Scene configuration
  • • Asset management
  • • Error handling

2. Integration Tests

  • • Unity app integration
  • • API endpoints
  • • Data synchronization
  • • Performance metrics

Future Enhancements

1. Planned Features

  • • Advanced object manipulation
  • • Scene templates
  • • Collaborative editing
  • • Real-time preview

2. Performance Improvements

  • • Asset optimization
  • • Scene loading optimization
  • • Memory management
  • • Network efficiency

Key Feature 2: Interactive Object Placement

Overview

The platform provides an intuitive system for placing and manipulating 3D objects within the VR/AR scenes directly from the Unity Admin Scene. This feature, often referred to as the "Tractor Beam Mode," allows administrators to select, move, rotate, and scale objects using mouse and keyboard controls.

Implementation Details

The interactive placement system is primarily managed by a set of scripts within the `Assets/Custom/Custom Scripts/` folder in the Unity project. The `AdminController.cs` script orchestrates the switching between UI interaction and object manipulation mode.

1. Mode Switching (`AdminController.cs`)

  • • Toggles between "UI Mode" and "Tractor Beam Mode" using the Tab key.
  • • In UI Mode, the mouse cursor is visible for UI interaction, and object manipulation scripts are disabled.
  • • In Tractor Beam Mode, the mouse cursor is locked, standard first-person controls are active, and object manipulation scripts are enabled.
// Simplified logic from AdminController.cs
public class AdminController : MonoBehaviour
{
    public KeyCode toggleModeKey = KeyCode.Tab;
    private bool isInUIMode = false;
    public AdminObjectSelector objectSelector;
    // ... other references ...

    void Update()
    {
        if (Input.GetKeyDown(toggleModeKey))
        {
            ToggleMode();
        }
        // ... handle movement/camera if not in UI mode ...
    }

    void ToggleMode()
    {
        isInUIMode = !isInUIMode;
        UpdateCursorState(); // Locks/unlocks cursor, enables/disables components
    }

    void UpdateCursorState()
    {
        Cursor.lockState = isInUIMode ? CursorLockMode.None : CursorLockMode.Locked;
        Cursor.visible = isInUIMode;
        if (objectSelector != null) objectSelector.enabled = !isInUIMode;
        // Enable/Disable Mover, Rotator, Scaler components...
    }
}

2. Object Selection (`AdminObjectSelector.cs`)

  • • Casts a ray from the camera based on mouse position in Tractor Beam Mode.
  • • Detects objects on the designated "Selectable" layer.
  • • Selects an object with a Left Mouse Click. Clicking again or clicking empty space deselects.
  • • Highlights the selected object by changing its color.
  • • Displays a `LineRenderer` as a "tractor beam" from the camera to the selected/hovered object.
// Simplified logic from AdminObjectSelector.cs
public class AdminObjectSelector : MonoBehaviour
{
    public Camera adminCamera;
    public LayerMask objectLayer;
    public LineRenderer tractorBeam;
    // ... color settings ...
    private GameObject selectedObject;

    void Update()
    {
        UpdateTractorBeam(); // Visualize beam

        if (Input.GetMouseButtonDown(0))
        {
            // Raycast logic...
            if (Physics.Raycast(..., out RaycastHit hit, ..., objectLayer))
            {
                // Selection logic (select, deselect, switch)
                // Change object color
            }
            else 
            {
                // Deselect if clicking empty space
            }
        }
    }
    // ... UpdateTractorBeam visualization logic ...
    public GameObject GetSelectedObject() { return selectedObject; }
}

3. Interaction Modes (`AdminModeController.cs`)

  • • Manages the current manipulation mode: `Move`, `Scale`, or `Rotate`.
  • • Cycles through modes using the Shift key.
  • • When in `Rotate` mode, manages the current rotation axis (`X`, `Y`, `Z`).
  • • Cycles through rotation axes using the R key.
// Logic from AdminModeController.cs
public class AdminModeController : MonoBehaviour
{
    public enum Mode { Move, Scale, Rotate }
    public static Mode CurrentMode = Mode.Move;
    public enum RotationAxis { X, Y, Z }
    public static RotationAxis CurrentRotationAxis = RotationAxis.Y;

    void Update()
    {
        // Cycle Mode with Shift key
        if (Input.GetKeyDown(KeyCode.LeftShift) || Input.GetKeyDown(KeyCode.RightShift)) { /* Cycle Mode */ }

        // Cycle Rotation Axis with R key (only in Rotate mode)
        if (CurrentMode == Mode.Rotate && Input.GetKeyDown(KeyCode.R)) { /* Cycle Axis */ }
    }
}

4. Object Manipulation (Mover, Rotator, Scaler)

  • • **`AdminObjectMover.cs`**: In `Move` mode, smoothly moves the selected object towards a point in front of the camera. The Mouse Scroll Wheel adjusts the distance.
  • • **`AdminObjectScaler.cs`**: In `Scale` mode, the Mouse Scroll Wheel uniformly scales the selected object.
  • • **`AdminObjectRotator.cs`**: In `Rotate` mode, the Mouse Scroll Wheel rotates the selected object around the axis specified by `AdminModeController.CurrentRotationAxis`.
// Simplified logic from AdminObjectMover.cs
public class AdminObjectMover : MonoBehaviour
{
    public AdminObjectSelector selector;
    public Transform holdPoint; // Usually the camera transform or a point in front
    private float holdDistance = 1f;
    
    void Update() {
        GameObject obj = selector.GetSelectedObject();
        if (obj != null) 
        {
            // Calculate target position based on camera orientation and distance
            Vector3 targetPosition = holdPoint.position + holdPoint.forward * holdDistance;
            // Smoothly move the object towards the target position
            obj.transform.position = Vector3.Lerp(obj.transform.position, targetPosition, Time.deltaTime * pullSpeed);

            // Adjust holdDistance with scroll wheel in Move mode
            if (AdminModeController.CurrentMode == AdminModeController.Mode.Move)
            {
                 float scroll = Input.GetAxis("Mouse ScrollWheel");
                 if (scroll != 0)
                 {
                     holdDistance += scroll * scrollSpeed;
                     holdDistance = Mathf.Clamp(holdDistance, 1f, 100f); // Limit distance
                 }
            }
        }
    }
}

// Simplified logic from AdminObjectRotator.cs
public class AdminObjectRotator : MonoBehaviour 
{
    public AdminObjectSelector selector;
    public Transform adminCamera; // Used to determine world rotation axis

    void Update() 
    {
        if (AdminModeController.CurrentMode == AdminModeController.Mode.Rotate) {
             GameObject obj = selector.GetSelectedObject();
             float scroll = Input.GetAxisRaw("Mouse ScrollWheel");
             if (obj != null && scroll != 0) 
             {
                 Vector3 rotationAxis = Vector3.zero;
                 // Determine world-space axis based on camera orientation
                 switch (AdminModeController.CurrentRotationAxis)
                 {
                     case AdminModeController.RotationAxis.X: rotationAxis = adminCamera.right; break;
                     case AdminModeController.RotationAxis.Y: rotationAxis = adminCamera.up; break;
                     case AdminModeController.RotationAxis.Z: rotationAxis = adminCamera.forward; break;
                 }
                 // Apply rotation around the calculated axis
                 obj.transform.Rotate(rotationAxis, scroll * rotationSpeed * Time.deltaTime, Space.World);
             }
        }
    }
}

// Simplified logic from AdminObjectScaler.cs
public class AdminObjectScaler : MonoBehaviour
{
     void Update() {
        if (AdminModeController.CurrentMode == AdminModeController.Mode.Scale) {
             GameObject obj = selector.GetSelectedObject();
             float scroll = Input.GetAxisRaw("Mouse ScrollWheel");
             if (obj != null && scroll != 0) 
             {
                 // Calculate new scale based on scroll input
                 Vector3 scaleChange = Vector3.one * scroll * scaleSpeed;
                 Vector3 newScale = obj.transform.localScale + scaleChange;
                 // Clamp scale to prevent it becoming too small or inverted
                 newScale = Vector3.Max(newScale, new Vector3(0.05f, 0.05f, 0.05f)); 
                 // Apply the new scale
                 obj.transform.localScale = newScale;
             }
        }
    }
}

5. Scene Configuration Persistence (`PortaltSceneExporter.cs`)

  • • Located in `Assets/Custom/Integration Scripts/`, this script handles saving the modified scene layout.
  • • It finds all objects with `SceneObjectMetadata` and updates the in-memory `SceneConfiguration` object (held by `PortaltSceneLoader`) with their current `Transform` values.
  • • The updated `SceneConfiguration` is serialized to JSON.
  • • A `UnityWebRequest` (HTTP PUT) sends the JSON data to the backend API endpoint, persisting the changes.
  • • Includes a Ctrl+S shortcut in the Unity Editor for convenience.
// Simplified logic from PortaltSceneExporter.cs
public class PortaltSceneExporter : MonoBehaviour
{
    public PortaltServerConfig serverConfig;
    public PortaltSceneLoader sceneLoader;

    // Main public method to trigger save
    public async Task SaveSceneToApi()
    {
        SceneConfiguration config = sceneLoader.CurrentSceneConfig;
        if (config == null) return false;
        UpdateSceneObjectTransforms(ref config);
        return await UploadSceneConfigurationAsync(config);
    }

    // Updates the SceneConfiguration object based on GameObjects in the scene
    public void UpdateSceneObjectTransforms(ref SceneConfiguration config)
    {
        SceneObjectMetadata[] sceneObjects = FindObjectsByType(...);
        foreach (var configObj in config.objects) {
            foreach (SceneObjectMetadata meta in sceneObjects) {
                if (meta.objectId == configObj.object_id) {
                    configObj.position = Vector3Serializable.FromVector3(meta.transform.position);
                    configObj.rotation = Vector3Serializable.FromVector3(meta.transform.eulerAngles);
                    configObj.scale = Vector3Serializable.FromVector3(meta.transform.localScale);
                    break; 
                }
            }
        }
        config.updatedAt = DateTime.UtcNow.ToString("o");
    }

    // Sends the configuration JSON to the API endpoint
    private async Task UploadSceneConfigurationAsync(SceneConfiguration config)
    {
        string url = serverConfig.GetSceneConfigUrl(config.scene_id);
        string jsonData = Newtonsoft.Json.JsonConvert.SerializeObject(config);
        using (UnityWebRequest request = UnityWebRequest.Put(url, jsonData)) {
            request.SetRequestHeader("Content-Type", "application/json");
            var operation = request.SendWebRequest();
            while (!operation.isDone) await Task.Yield();
            return request.result == UnityWebRequest.Result.Success;
        }
    }

    // Editor shortcut
    private void Update() {
        if (Input.GetKeyDown(KeyCode.S) && (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))) {
            _ = SaveSceneToApi(); 
        }
    }
}

Integration with Scene Data

  • • Objects intended for manipulation should have the `SceneObjectMetadata.cs` script attached and be placed on the "Selectable" layer.
  • • Changes made (position, rotation, scale) modify the object's `Transform` component directly in the Unity scene.
  • • The `PortaltSceneExporter.cs` script (in `Integration Scripts`) is responsible for reading the final `Transform` values of these objects and saving them back to the scene configuration database via the backend API.

Future Enhancements

  • • Implementing visual gizmos for more precise manipulation.
  • • Adding snapping functionality for easier alignment.
  • • Support for multi-object selection and manipulation.
  • • Undo/Redo functionality for placement actions.

Key Feature 3: User Authentication and Management

Overview

The application implements a comprehensive authentication and user management system using Clerk, a modern authentication service. The system provides secure user authentication, organization management, and role-based access control.

Implementation Details

2.1 Authentication Provider
<!-- src/pages/_app.tsx -->
import { ClerkProvider } from "@clerk/nextjs";

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ClerkProvider {...pageProps}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </ClerkProvider>
  );
}
2.2 Authentication Pages
<!-- src/pages/sign-in/[[...rest]]/index.tsx -->
import { SignIn } from "@clerk/nextjs";

export default function LoginPage() {
  return (
    <main className="h-screen w-full flex items-center justify-center bg-gray-50">
      <div className="w-full max-w-md p-8">
        <h1 className="text-2xl font-bold text-center mb-8 text-gray-800">
          Welcome Back
        </h1>
        <SignIn />
      </div>
    </main>
  );
}

3. Middleware Implementation

The application implements a custom middleware to handle authentication and organization selection:

// src/middleware.ts
export default clerkMiddleware(async (auth, req) => {
  const { userId, sessionClaims, redirectToSignIn } = await auth();

  // Handle public routes
  if(isPublicRoute(req)){
    return NextResponse.next();
  }

  // Handle unauthenticated users
  if (!userId) {
    return redirectToSignIn();
  }

  // Handle organization selection
  if (userId && !sessionClaims.org_id && 
      req.nextUrl.pathname !== "/organization-select") {
    return Response.redirect(new URL("/organization-select", req.url));
  }

  return NextResponse.next();
});

4. Organization Management

The application implements organization-based access control:

// src/pages/organization-select/index.tsx
import { OrganizationList } from "@clerk/nextjs";

export default function OrganizationSelectPage() {
  return (
    <main className="h-screen w-full flex items-center justify-center bg-gray-50">
      <div className="w-full max-w-md p-8">
        <h1 className="text-2xl font-bold text-center mb-8 text-gray-800">
          Select Organization
        </h1>
        <OrganizationList
          afterCreateOrganizationUrl="/"
          afterSelectOrganizationUrl="/"
        />
      </div>
    </main>
  );
}

5. API Authentication

All API routes implement organization-based authentication:

// Example API route with authentication
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { orgId } = getAuth(req);
  if (!orgId) return res.status(401).json({ message: "Unauthorized" });
  
  // Protected route logic
}

Authentication Flow

Authentication Flow Diagram

Flow Description:

  • • User accesses application
  • • System checks authentication status
  • • Unauthenticated users are redirected to sign in
  • • After authentication, organization selection is required
  • • Organization selection grants access to protected content

Security Features

Session Management
  • • Secure session handling through Clerk
  • • Automatic session expiration
  • • Multi-device session support
Organization Isolation
  • • Data isolation per organization
  • • Organization-specific access control
  • • Organization switching capability
API Security
  • • Organization-based API access
  • • Secure token handling
  • • Rate limiting support
Public Routes
  • • Defined public access paths
  • • Secure public route handling
  • • Authentication bypass for specific routes

Key Feature 5: Data Storage and Synchronization

Overview

The data storage and synchronization system implements a hybrid approach using IBM Cloud Object Storage (COS) for asset storage and SQLite for scene configuration data. This architecture ensures efficient storage of large assets while maintaining quick access to configuration data.

Implementation

COS Client Configuration


import { S3 } from 'aws-sdk';

const cosClient = new S3({
    endpoint: process.env.COS_ENDPOINT,
    credentials: {
        accessKeyId: process.env.COS_ACCESS_KEY_ID,
        secretAccessKey: process.env.COS_SECRET_ACCESS_KEY
    },
    region: process.env.COS_REGION
});
                                

File Upload and URL Generation


async function uploadAsset(file: File, key: string): Promise {
    const params = {
        Bucket: process.env.COS_BUCKET_NAME,
        Key: key,
        Body: file,
        ContentType: file.type
    };

    await cosClient.upload(params).promise();
    
    return cosClient.getSignedUrl('getObject', {
        Bucket: process.env.COS_BUCKET_NAME,
        Key: key,
        Expires: 3600 // URL expires in 1 hour
    });
}
                                

Scene Configuration Storage


interface SceneConfig {
    id: string;
    name: string;
    assets: Array<{
        id: string;
        url: string;
        type: string;
        position: { x: number; y: number; z: number; }
    }>;
}

async function saveSceneConfig(config: SceneConfig): Promise {
    await db.run(`
        INSERT OR REPLACE INTO scene_configs (id, name, config)
        VALUES (?, ?, ?)
    `, [config.id, config.name, JSON.stringify(config)]);
}
                                

API Handling


app.post('/api/scenes/:id/assets', async (req, res) => {
    try {
        const file = req.files.asset;
        const sceneId = req.params.id;
        const key = `scenes/${sceneId}/${file.name}`;
        
        const url = await uploadAsset(file, key);
        const config = await getSceneConfig(sceneId);
        
        config.assets.push({
            id: uuidv4(),
            url,
            type: file.type,
            position: req.body.position
        });
        
        await saveSceneConfig(config);
        
        res.json({ success: true, url });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});
                                

Data Flow Diagram

Data Flow Diagram

Unity Integration

1. Scene Configuration Access

The Unity application accesses scene configurations through a dedicated API client that handles:

// Example Unity API call
const response = await fetch(`/api/scenes-configuration/${sceneId}?orgId=${orgId}`);
const config = await response.json();

2. Asset Access

Unity applications can access assets through public COS URLs:

// Example asset URL structure
const assetUrl = `https://${bucketName}.s3.${region}.cloud-object-storage.appdomain.cloud/documents/${assetKey}`;

Security Implementation

1. Access Control

  • • Organization-based access control
  • • Signed URLs for COS assets with expiration
  • • Public URLs for Unity app access
  • • Token-based authentication for API access

2. URL Security

// Generate short-lived signed URL for sensitive operations
export async function getSignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
  const cos = getCosClient();
  return cos.getSignedUrl('getObject', {
    Bucket: BUCKET_NAME,
    Key: key,
    Expires: expiresIn,
  });
}

Data Synchronization

1. Real-time Updates

  • • Scene configurations are updated in real-time
  • • Unity apps poll for configuration updates
  • • Asset changes are immediately reflected

2. Conflict Resolution

  • • Last-write-wins strategy for scene configurations
  • • Version tracking for asset updates
  • • Atomic updates for critical operations

Performance Optimization

1. Caching Strategy

  • • In-memory caching of frequently accessed configurations
  • • Local storage of commonly used assets
  • • CDN integration for asset delivery

2. Batch Operations

// Example batch asset upload
const uploadPromises = files.map(file => 
  uploadDocument(file.buffer, file.name, file.mimetype)
);
await Promise.all(uploadPromises);

Error Handling

1. Storage Errors

try {
  await uploadDocument(file, filename, mimeType);
} catch (error) {
  console.error('Error uploading to COS:', {
    error: error instanceof Error ? {
      message: error.message,
      stack: error.stack,
      name: error.name
    } : error,
    bucket: BUCKET_NAME,
    filename
  });
  throw error;
}

2. Synchronization Errors

  • • Retry mechanisms for failed uploads
  • • Fallback options for unavailable assets
  • • Error reporting and logging

Environment Configuration

# Required environment variables
COS_ENDPOINT=https://s3.us-east.cloud-object-storage.appdomain.cloud
COS_ACCESS_KEY_ID=your_access_key_id
COS_SECRET_ACCESS_KEY=your_secret_access_key
COS_REGION=us-east
COS_BUCKET_NAME=your_bucket_name

# Database configuration
SQLITE_PATH=./data/scene_configs.db
MAX_CACHE_SIZE=100MB
ASSET_CACHE_DURATION=3600

Testing Considerations

  • Storage Tests:
    • Asset upload/download verification
    • Configuration CRUD operations
    • URL generation and expiration
    • Error handling and retry logic
  • Integration Tests:
    • Unity client integration
    • Synchronization mechanisms
    • Performance under load
    • Security implementation

Future Enhancements

  • Advanced caching mechanisms with predictive prefetching
  • Multi-region asset distribution
  • Real-time collaborative editing features
  • Enhanced conflict resolution strategies
  • Automated asset optimization pipeline

Key Feature 6: Viewer Scene Implementation

Overview

The Viewer Scene provides the end-user experience, allowing users (typically in VR) to join and explore the 3D environments configured using the Admin Scene or the web dashboard. It focuses on loading pre-defined scene layouts, connecting users via Ubiq networking, and displaying the assets accurately based on the saved configuration.

Implementation Details

The Viewer Scene relies on several key scripts from the `Assets/Custom/Integration Scripts/` folder to manage the connection, loading, and display process.

1. Entry Point & Configuration (`VRViewerManager.cs`)

  • • Acts as the main controller for the Viewer Scene.
  • • Holds configuration (`PortaltServerConfigV`) containing the backend server address and the specific `activityId` to load.
  • • Manages the initial setup, ensuring necessary components like `PortaltSceneLoader` and `JoincodeInputManager` are ready.
  • • Can be configured to require a join code input via the `JoincodeInputManager` before proceeding.
  • • Initiates the scene loading process once configuration and connection details are available.
// Simplified logic from VRViewerManager.cs
public class VRViewerManager : MonoBehaviour
{
    public PortaltServerConfigV serverConfig;
    public string activityIdToLoad;
    public bool requireJoincode = true;
    public JoincodeInputManager joincodeManager;
    private PortaltSceneLoader sceneLoader;

    void Start()
    {
        SetupComponents(); // Ensure Loader, API Client etc. exist

        if (requireJoincode && string.IsNullOrEmpty(serverConfig.joinCode))
        {
            joincodeManager.ShowJoincodeUI(); // Wait for user input
        }
        else if (loadOnStart)
        {
            StartCoroutine(LoadWithDelay()); // Start loading process
        }
    }

    public void LoadFromJoinCode() 
    {
        // Fetch configuration from backend using join code / activity ID
        StartCoroutine(FetchAndLoadFromJoinUrl());
    }

    private IEnumerator FetchAndLoadFromJoinUrl()
    {
        // Use UnityWebRequest to get SceneConfiguration JSON from join URL
        // ... On success:
        // SceneConfiguration config = ParseJson(downloadHandler.text);
        // sceneLoader.LoadScene(config); // Pass config to loader
    }

    // Coroutine to handle the web request
    private IEnumerator FetchConfigCoroutine(string url, Action onSuccess, Action onError)
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
        {
            yield return webRequest.SendWebRequest();

            if (webRequest.result == UnityWebRequest.Result.Success)
            {
                try
                {
                    SceneConfiguration config = JsonUtility.FromJson(webRequest.downloadHandler.text);
                    onSuccess?.Invoke(config);
                }
                catch (Exception e)
                {
                    onError?.Invoke($"Failed to parse scene configuration: {e.Message}");
                }
            }
            else
            {
                onError?.Invoke($"Failed to fetch scene configuration: {webRequest.error}");
            }
        }
    }
}

2. Scene Loading & Object Instantiation (`PortaltSceneLoader.cs`)

  • • Receives the `SceneConfiguration` data (fetched by `VRViewerManager` or potentially other triggers).
  • • Clears any previously loaded objects to ensure a fresh scene.
  • • Iterates through the `objects` array within the `SceneConfiguration`.
  • • For each object, it asynchronously downloads the 3D model (glTF/GLB) from the specified `modelUrl` using `ModelDownloader` internally via `GLTFUtility`.
  • • Instantiates the downloaded model into the Unity scene.
  • • Applies the exact `position`, `rotation`, and `scale` from the configuration data to the instantiated object's `Transform`.
  • • Manages a loading screen via `LoadingScreenManager` during the process.
// Simplified logic from PortaltSceneLoader.cs
public class PortaltSceneLoader : MonoBehaviour
{
    private List loadedObjects = new List();

    public async Task LoadScene(SceneConfiguration config)
    {
        ClearScene(); // Remove old objects
        ShowLoadingScreen(config.scene_id);

        currentSceneConfig = config;

        List loadingTasks = new List();
        foreach (SceneObject objData in config.objects)
        {
            loadingTasks.Add(LoadAndPlaceObjectAsync(objData));
        }
        await Task.WhenAll(loadingTasks);

        HideLoadingScreen();
        return true;
    }

    // Renamed for clarity
    private async Task LoadAndPlaceObjectAsync(SceneObject objData)
    {
        GameObject instantiatedModel = null;
        try
        {
            // Asynchronously download and import the glTF/GLB model
            // GLTFUtility.LoadModelAsync handles the download and parsing
            instantiatedModel = await Siccity.GLTFUtility.GLTFImporter.LoadModelAsync(objData.modelUrl);

            if (instantiatedModel != null)
            {
                // Apply transform data from the configuration
                ApplyTransform(instantiatedModel, objData);

                // Add metadata component (useful for identification or interaction)
                SceneObjectMetadata metadata = instantiatedModel.AddComponent();
                metadata.Initialize(objData.object_id, objData.modelUrl); 

                // Optionally add colliders (needed for physics or selection)
                AddColliders(instantiatedModel);

                // Keep track of the object
                loadedObjects.Add(instantiatedModel);
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to load model {objData.object_id} from {objData.modelUrl}: {e.Message}");
            if (instantiatedModel != null) Destroy(instantiatedModel); // Clean up partially loaded object
        }
    }

    // Renamed for clarity
    private void ApplyTransform(GameObject modelInstance, SceneObject objData)
    {
        // Convert stored position, rotation, scale to Unity's Vector3 and Quaternion
        modelInstance.transform.position = objData.position.ToVector3();
        modelInstance.transform.rotation = Quaternion.Euler(objData.rotation.ToVector3()); // Convert Euler angles
        modelInstance.transform.localScale = objData.scale.ToVector3();
    }

    // Helper to dynamically add colliders based on mesh bounds
    private void AddColliders(GameObject modelInstance)
    {
        // Example: Add a BoxCollider encompassing all child renderers
        Bounds combinedBounds = new Bounds();
        Renderer[] renderers = modelInstance.GetComponentsInChildren();
        if (renderers.Length > 0)
        {
            combinedBounds = renderers[0].bounds;
            foreach (Renderer renderer in renderers)
            {
                combinedBounds.Encapsulate(renderer.bounds);
            }

            BoxCollider collider = modelInstance.AddComponent();
            // Adjust collider center and size relative to the model's root transform
            collider.center = modelInstance.transform.InverseTransformPoint(combinedBounds.center);
            collider.size = modelInstance.transform.InverseTransformVector(combinedBounds.size);
        }
    }

    public void ClearScene()
    {
        foreach (var obj in loadedObjects) { Destroy(obj); }
        loadedObjects.Clear();
        // Destroy collider visualizations too
    }
}

3. Network Connection (`JoincodeInputManager.cs` & Ubiq)

  • • Handles the user interface for entering a join code if required by `VRViewerManager`.
  • • Configures the Ubiq `RoomClient` connection details (IP address, port) based on the server configuration.
  • • Uses the provided join code to connect to the correct Ubiq room, enabling shared experiences between viewers.
  • • The actual synchronization of player avatars and any dynamic interactions relies on the underlying Ubiq networking components attached to relevant prefabs.
// Simplified logic from JoincodeInputManager.cs related to connection
public class JoincodeInputManager : MonoBehaviour
{
    public InputField joincodeInputField; 
    public GameObject uiPanel; 
    private RoomClient roomClient;
 
    void Awake()
    {
        roomClient = FindObjectOfType();
        // ... other setup ...
    }
 
    public void ShowJoincodeUI()
    {
        uiPanel.SetActive(true);
    }
 
    public void AttemptJoinWithJoincode()
    {
        string joincode = joincodeInputField.text;
        if (!string.IsNullOrEmpty(joincode))
        {
            // Potentially fetch server details based on joincode from an API first
            // ... (Assuming server details are already in serverConfig for simplicity)

            // Configure RoomClient connection definition
            var connection = ScriptableObject.CreateInstance();
            connection.sendToIp = serverConfig.serverIp; 
            connection.sendToPort = serverConfig.serverPort;
            connection.type = ConnectionDefinition.Type.TcpClient; // Or appropriate type
            roomClient.SetDefaultServer(connection);

            // Use joincode to join the specific room
            roomClient.Join(joincode);
            
            uiPanel.SetActive(false); 
            // Trigger scene loading via VRViewerManager or event
            FindObjectOfType()?.LoadFromJoinCode(); 
        }
    }
}

Key Differences from Admin Scene

  • • **No Editing**: The Viewer Scene does not include the interactive object placement tools (`AdminObjectSelector`, `Mover`, `Rotator`, `Scaler`).
  • • **Direct Load**: Typically loads a specific scene configuration fetched directly based on an activity ID or join code.
  • • **Ubiq Focus**: Relies heavily on Ubiq for establishing a connection to a shared room defined by the join code.
  • • **End-User Interaction**: Designed for consumption and exploration, with interaction potentially handled by standard VR controllers and Ubiq components rather than admin tools.

Future Enhancements

  • • Adding simple interaction capabilities (e.g., grabbing, basic physics).
  • • Implementing teleportation or other VR locomotion methods.
  • • Displaying dynamic information or annotations linked to objects.
  • • Optimizing loading performance for very large scenes or models.