Skip to content

Descripción General de la Arquitectura

Oroya Animate sigue una arquitectura desacoplada donde la representación de la escena es completamente independiente de la tecnología de renderizado.


Principio fundamental

“Define once, render anywhere.” (Define una vez, renderiza en cualquier lugar.) El scene graph es la única fuente de verdad. Los renderers son traductores.

graph TD
    subgraph "Capa de entrada"
        UC["Código de usuario"]
        GLTF["Archivos glTF / GLB"]
        JSON["JSON Serializado"]
    end

    subgraph "@joroya/core — Motor agnóstico"
        SG["Scene Graph"]
        N["Node"]
        T["Transform"]
        G["Geometry"]
        M["Material"]
        C["Camera"]
        SER["Serializer"]
        MATH["Math (Matrix4)"]
    end

    subgraph "Capa de salida"
        R3["@joroya/renderer-three"]
        RS["@joroya/renderer-svg"]
        R_FUTURE["Futuro: Canvas2D, WebGPU..."]
    end

    subgraph "Resultado"
        WEBGL["WebGL Canvas (píxeles)"]
        SVG["SVG String (vectores)"]
    end

    UC -->|"construye"| SG
    GLTF -->|"@joroya/loader-gltf"| SG
    JSON -->|"deserialize()"| SG
    SG -->|"serialize()"| JSON

    SG --> N
    N --> T
    N --> G
    N --> M
    N --> C
    SG --> MATH

    SG -->|"mount + render"| R3
    SG -->|"renderToSVG()"| RS
    SG -.->|"futuro"| R_FUTURE

    R3 --> WEBGL
    RS --> SVG

Capas de la arquitectura

CapaPaqueteResponsabilidadDependencias
Core@joroya/coreScene graph, componentes, transforms, serialización, mathuuid (única dependencia)
Renderer 3D@joroya/renderer-threeTraducción a Three.js WebGL@joroya/core, three
Renderer SVG@joroya/renderer-svgGeneración de SVG puro@joroya/core
Loader glTF@joroya/loader-gltfImportación de modelos 3D@joroya/core, three

Grafo de dependencias

graph BT
    CORE["@joroya/core"]
    R3["@joroya/renderer-three"]
    RS["@joroya/renderer-svg"]
    LG["@joroya/loader-gltf"]
    THREE["three (npm)"]
    UUID["uuid (npm)"]
    DEMO["apps/demo-react"]

    CORE -->|"depende de"| UUID
    R3 -->|"depende de"| CORE
    R3 -->|"depende de"| THREE
    RS -->|"depende de"| CORE
    LG -->|"depende de"| CORE
    LG -->|"depende de"| THREE
    DEMO -->|"depende de"| CORE
    DEMO -->|"depende de"| R3

Regla clave: Las flechas de dependencia son unidireccionales y siempre apuntan hacia @joroya/core. El core nunca importa de los renderers ni de los loaders.


El patrón “Compilador”

Los renderers funcionan como compiladores: traducen una representación intermedia (el scene graph) a un formato de salida específico.

flowchart LR
    IR["Scene Graph\n(Representación intermedia)"] -->|"ThreeRenderer"| OUT1["THREE.Scene\nTHREE.Mesh\nTHREE.Camera"]
    IR -->|"renderToSVG()"| OUT2["<svg>\n  <path/>\n</svg>"]
    IR -.->|"Futuro: WebGPU"| OUT3["GPUBuffer\nGPURenderPipeline"]
ConceptoCompilador clásicoOroya Animate
Código fuenteArchivo .cCódigo de usuario (TypeScript)
Representación intermediaAST / IRScene Graph
Backendx86, ARM, WASMThree.js, SVG, WebGPU
SalidaCódigo máquinaPíxeles, vectores

Este patrón permite:

  • Agregar backends sin modificar el core.
  • Testear sin renderer — la lógica vive en el scene graph.
  • Server-side rendering — el SVG renderer funciona en Node.js sin DOM.

Ciclo de vida del renderizado

sequenceDiagram
    participant U as Código de usuario
    participant S as Scene
    participant N as Node
    participant T as Transform
    participant R as Renderer

    Note over U,R: FASE 1 — Preparación
    U->>S: new Scene()
    U->>N: new Node('box')
    U->>N: addComponent(createBox(...))
    U->>N: addComponent(new Material(...))
    U->>S: scene.add(node)

    Note over U,R: FASE 2 — Montaje
    U->>R: renderer.mount(scene)
    R->>S: scene.traverse(callback)
    R->>R: Crear objetos del backend
    R->>R: Detectar cámara activa

    Note over U,R: FASE 3 — Bucle de renderizado
    loop requestAnimationFrame
        U->>T: transform.rotation = {...}
        U->>T: transform.updateLocalMatrix()
        U->>R: renderer.render()
        R->>S: scene.updateWorldMatrices()
        S->>N: node.updateWorldMatrix(parentMatrix)
        N->>T: worldMatrix = parent × local
        R->>R: Sincronizar con backend
        R->>R: Dibujar frame
    end

    Note over U,R: FASE 4 — Limpieza
    U->>R: renderer.dispose()

Detalle de cada fase

FaseAcciónQuién la ejecuta
1. PreparaciónConstruir el scene graph con nodos, componentes y relaciones padre-hijoCódigo de usuario
2. Montajerenderer.mount(scene) — recorrer el árbol y crear los objetos del backendRenderer
3. Bucle de renderizadoMutar transforms → updateLocalMatrix()renderer.render() → propagar matrices → dibujarCódigo de usuario + Renderer
4. Limpiezarenderer.dispose() — liberar recursos GPU/memoriaCódigo de usuario

Modelo Entity-Component System (ECS) simplificado

Oroya usa un ECS ligero donde:

Término ECSEquivalente en OroyaDescripción
EntityNodeContenedor con ID y jerarquía
ComponentTransform, Geometry, Material, CameraDatos adjuntos a un nodo
SystemRenderers, updateWorldMatrices()Lógica que procesa componentes
graph LR
    subgraph "Entity (Node)"
        N["Node 'player'"]
    end

    subgraph "Componentes"
        T["Transform\nposition, rotation, scale"]
        G["Geometry\nBox 1×1×1"]
        M["Material\ncolor: blue"]
    end

    subgraph "Sistemas"
        S1["updateWorldMatrices()\nPropaga matrices"]
        S2["ThreeRenderer.render()\nDibuja con Three.js"]
    end

    N --> T
    N --> G
    N --> M
    T --> S1
    G --> S2
    M --> S2

Reglas del ECS

  1. Un componente por tipo por nodo — No se pueden tener dos Geometry en un mismo nodo.
  2. Transform es automático — Todos los nodos lo tienen desde su creación.
  3. Los componentes son datos — No contienen lógica de renderizado.
  4. Los renderers son los “systems” — Leen componentes y producen salida visual.

Extensibilidad

Agregar un nuevo renderer

class MyRenderer {
  private scene: Scene | null = null;

  mount(scene: Scene): void {
    this.scene = scene;
    scene.traverse(node => {
      const geo = node.getComponent<Geometry>(ComponentType.Geometry);
      const mat = node.getComponent<Material>(ComponentType.Material);
      // Crear objetos del motor/framework destino
    });
  }

  render(): void {
    if (!this.scene) return;
    this.scene.updateWorldMatrices();
    this.scene.traverse(node => {
      // Leer node.transform.worldMatrix
      // Sincronizar con los objetos del motor
    });
    // Dibujar frame
  }

  dispose(): void { /* liberar recursos */ }
}

Agregar un nuevo loader

async function loadMyFormat(url: string): Promise<Scene> {
  const data = await fetch(url).then(r => r.json());
  const scene = new Scene();

  for (const obj of data.objects) {
    const node = new Node(obj.name);
    node.addComponent(createBox(obj.width, obj.height, obj.depth));
    node.addComponent(new Material({ color: obj.color }));
    node.transform.position = obj.position;
    scene.add(node);
  }

  return scene;
}

Estructura del monorepo

oroya-animate/
├── packages/
│   ├── core/               → Motor agnóstico (Scene, Node, Components, Math)
│   ├── renderer-three/      → Backend Three.js WebGL
│   ├── renderer-svg/        → Backend SVG puro
│   └── loader-gltf/         → Importador de modelos glTF
├── apps/
│   └── demo-react/          → Aplicación demo (Vite + React)
├── docs/                    → Documentación del proyecto
└── package.json             → Raíz del monorepo (pnpm workspaces)

Decisiones de diseño

DecisiónAlternativa rechazadaRazón
Scene graph como IRAPI directa sobre Three.jsPermite múltiples backends y testing sin GPU
ECS simplificado (1 comp/tipo)ECS completo con SystemsMenor complejidad para el alcance actual
Quaterniones para rotaciónÁngulos de EulerSin gimbal lock, interpolación natural (SLERP)
Matrices column-majorMatrices row-majorCompatible con WebGL y Three.js
uuid para IDs de nodoIDs incrementalesIDs únicos globalmente, necesario para serialización
tsup como bundlertsc, rollup, esbuildDTS + CJS + ESM en un solo tool con configuración mínima
pnpm workspacesnpm/yarn workspacesMás rápido, deduplicación estricta, mejor para monorepos