Skip to content

Oroya Animateをはじめよう

ようこそ!このガイドでは、Web向けのレンダラー非依存シーングラフエンジン Oroya Animate を使って、最初の3Dシーンを構築する方法を説明します。

Oroya Animateはシーンの定義とレンダリングバックエンドを分離しています。@joroya/core を使ってシーンを一度記述し、Three.jsによるWebGL、SVGによる静的グラフィックス、Canvas 2D、または将来のバックエンドからレンダリング方法を選択できます。


インストール

お好みのパッケージマネージャーを使って、npmから必要なパッケージをインストールしてください:

# npm
npm install @joroya/core @joroya/renderer-three

# yarn
yarn add @joroya/core @joroya/renderer-three

# pnpm
pnpm add @joroya/core @joroya/renderer-three

利用可能なパッケージ

パッケージ説明
@joroya/coreシーングラフ、ノード、コンポーネント、シリアライゼーション、数学ユーティリティ
@joroya/renderer-threeThree.jsベースのWebGLレンダラー
@joroya/renderer-svg2Dパスベースグラフィックス用SVGレンダラー
@joroya/renderer-canvas2d2Dラスターグラフィックス用Canvas 2Dレンダラー
@joroya/loader-gltfglTFモデルをOroyaシーンに読み込み

注意: @joroya/renderer-three@joroya/loader-gltfthree をpeer dependencyとして持っています。プロジェクトにインストールされていることを確認してください:npm install three


クイックスタート — 回転するキューブ

この最小限の例では、カメラ、ライティング、赤いキューブを含むシーンを作成し、Three.jsでレンダリングします。

1. HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>Oroya Animate - クイックスタート</title>
  <style>
    body { margin: 0; overflow: hidden; }
    canvas { display: block; }
  </style>
</head>
<body>
  <canvas id="oroya-canvas"></canvas>
  <script type="module" src="./main.ts"></script>
</body>
</html>

2. シーンのセットアップ (main.ts)

import {
  Scene, Node, createBox, Material,
  Camera, CameraType, Light, LightType,
} from '@joroya/core';
import { ThreeRenderer } from '@joroya/renderer-three';

// --- シーン ---
const scene = new Scene();

// --- カメラ ---
const cameraNode = new Node('main-camera');
cameraNode.addComponent(new Camera({
  type: CameraType.Perspective,
  fov: 75,
  aspect: window.innerWidth / window.innerHeight,
  near: 0.1,
  far: 1000,
}));
cameraNode.transform.position.z = 5;
scene.add(cameraNode);

// --- ライティング ---
const ambient = new Node('ambient');
ambient.addComponent(new Light({ type: LightType.Ambient, intensity: 0.4 }));
scene.add(ambient);

const sun = new Node('sun');
sun.addComponent(new Light({
  type: LightType.Directional,
  intensity: 0.8,
  target: { x: 0, y: 0, z: 0 },
}));
sun.transform.position = { x: 3, y: 6, z: 4 };
scene.add(sun);

// --- キューブ ---
const cubeNode = new Node('my-cube');
cubeNode.addComponent(createBox(1, 1, 1));
cubeNode.addComponent(new Material({ color: { r: 1, g: 0.2, b: 0.2 } }));
scene.add(cubeNode);

// --- レンダラー ---
const canvas = document.getElementById('oroya-canvas') as HTMLCanvasElement;
const renderer = new ThreeRenderer({
  canvas,
  width: window.innerWidth,
  height: window.innerHeight,
});
renderer.mount(scene);

// --- アニメーションループ ---
function animate(time: number) {
  time *= 0.001; // 秒に変換
  requestAnimationFrame(animate);

  const speed = 0.5;
  cubeNode.transform.rotation.x = Math.sin(time * speed) * 2;
  cubeNode.transform.rotation.y = Math.cos(time * speed) * 2;
  cubeNode.transform.updateLocalMatrix();

  renderer.render();
}
requestAnimationFrame(animate);

これだけです!画面上に赤いキューブが回転して表示されるはずです。


主要コンセプト

シーン (Scene)

Scene はトップレベルのコンテナです。ルート Node を保持し、ノードの追加/削除、グラフの走査、ワールド行列の更新のためのメソッドを提供します。

const scene = new Scene();
scene.add(someNode);                    // ルートに追加
scene.add(childNode, parentNode);       // 特定の親の下に追加
scene.remove(someNode);                 // 親から削除
scene.findNodeByName('my-cube');        // 名前で検索
scene.findNodeById(id);                 // ユニークIDで検索
scene.traverse((node) => { /* ... */ });

ノード (Nodes)

ノードはシーングラフの構成要素です。親子階層を形成し、子ノードは親のワールド変換を継承します。

const parent = new Node('solar-system');
const earth  = new Node('earth');
const moon   = new Node('moon');

parent.add(earth);
earth.add(moon);   // 月は地球の変換を継承

scene.add(parent);

コンポーネント (Components)

コンポーネントはノードにデータや動作を付加します。すべてのノードはデフォルトで Transform コンポーネントを持ちます。

コンポーネント説明
Transform位置 (Vec3)、回転 (Quat)、スケール (Vec3)。値を変更した後は updateLocalMatrix() を呼び出してください。
Geometryノードの形状を定義します。ファクトリ関数で作成します。
Material外観を定義 — 色、透明度、fill/stroke (SVG用)、metalness/roughness (PBR用)。
Camera視点を定義 — パースペクティブまたはオルソグラフィック。
Light光源を定義 — Ambient、Directional、Point、Spot。
// コンポーネントの追加
node.addComponent(createBox(2, 2, 2));
node.addComponent(new Material({ color: { r: 0, g: 1, b: 0 }, opacity: 0.8 }));

// コンポーネントの取得
node.hasComponent(ComponentType.Geometry); // true
const geo = node.getComponent<Geometry>(ComponentType.Geometry);

ジオメトリプリミティブ

Oroyaはジオメトリコンポーネントを作成するためのファクトリ関数を提供しています:

ボックス (Box)

import { createBox } from '@joroya/core';

const box = createBox(2, 1, 3); // 幅, 高さ, 奥行き
node.addComponent(box);

球 (Sphere)

import { createSphere } from '@joroya/core';

const sphere = createSphere(0.5, 32, 32); // 半径, 横分割数, 縦分割数
node.addComponent(sphere);

Path2D (SVG/Canvas2Dレンダリング用)

import { createPath2D } from '@joroya/core';

const triangle = createPath2D([
  { command: 'M', args: [50, 0] },
  { command: 'L', args: [100, 100] },
  { command: 'L', args: [0, 100] },
  { command: 'Z', args: [] },
]);
node.addComponent(triangle);

テキスト

import { createText } from '@joroya/core';

const text = createText('こんにちは!', {
  fontSize: 24,
  fontFamily: 'sans-serif',
  fontWeight: 'bold',
  textAnchor: 'middle',
});
node.addComponent(text);

マテリアル

Material コンポーネントはノードの視覚的な外観を制御します。そのプロパティは選択したレンダラーに応じて適応します。

import { Material } from '@joroya/core';

// 3D用 (Three.jsレンダラー)
node.addComponent(new Material({
  color: { r: 0.2, g: 0.5, b: 1.0 },
  opacity: 0.9,
  metalness: 0.3,
  roughness: 0.5,
}));

// 2D用 (SVGまたはCanvas2Dレンダラー)
node.addComponent(new Material({
  fill: { r: 1, g: 0.8, b: 0 },
  stroke: { r: 0, g: 0, b: 0 },
  strokeWidth: 2,
}));

色は ColorRGB 形式を使用し、各チャンネルは 0 から 1 の範囲です。


ライティング

Light コンポーネントはシーン内の光源を定義します。4つのタイプがサポートされています:

import { Light, LightType } from '@joroya/core';

// アンビエントライト — すべての方向に均一な照明
const ambient = new Node('ambient');
ambient.addComponent(new Light({
  type: LightType.Ambient,
  color: { r: 1, g: 1, b: 1 },
  intensity: 0.4,
}));
scene.add(ambient);

// ディレクショナルライト — 平行光線(太陽光のような)
const sun = new Node('sun');
sun.addComponent(new Light({
  type: LightType.Directional,
  intensity: 0.9,
  target: { x: 0, y: 0, z: 0 },
}));
sun.transform.position = { x: 5, y: 10, z: 5 };
scene.add(sun);

// ポイントライト — ある点から全方向に発光
const bulb = new Node('bulb');
bulb.addComponent(new Light({
  type: LightType.Point,
  color: { r: 1, g: 0.8, b: 0.4 },
  intensity: 2.0,
  distance: 15,
  decay: 2,
}));
bulb.transform.position = { x: -3, y: 4, z: 2 };
scene.add(bulb);

// スポットライト — 円錐形の光(懐中電灯のような)
const spot = new Node('spot');
spot.addComponent(new Light({
  type: LightType.Spot,
  intensity: 3.0,
  angle: 0.5,
  penumbra: 0.4,
  target: { x: 0, y: 0, z: 0 },
}));
spot.transform.position = { x: 0, y: 8, z: 0 };
scene.add(spot);

カメラ

カメラはシーンがレンダリングされる視点を定義します。ノードにアタッチし、ノードのtransformを使って位置を決定します。

import { Camera, CameraType } from '@joroya/core';

const cameraNode = new Node('camera');
cameraNode.addComponent(new Camera({
  type: CameraType.Perspective,
  fov: 60,
  aspect: 16 / 9,
  near: 0.1,
  far: 500,
}));

// 位置と向き
cameraNode.transform.position = { x: 0, y: 3, z: 10 };
cameraNode.transform.updateLocalMatrix();

scene.add(cameraNode);

ThreeRenderer はシーン内で最初に見つかったカメラを使用します。存在しない場合は、z = 5 にデフォルトのパースペクティブカメラを作成します。


レンダラー

Three.js (WebGL)

インタラクティブで高性能な3Dシーンには @joroya/renderer-three を使用します。

import { ThreeRenderer } from '@joroya/renderer-three';

const renderer = new ThreeRenderer({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: 800,
  height: 600,
  dpr: window.devicePixelRatio, // オプション
});

renderer.mount(scene);
renderer.enableOrbitControls(); // マウスによるカメラ操作

function loop() {
  renderer.render();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

// 完了時にクリーンアップ
renderer.dispose();

SVG

スケーラブルな2Dベクター出力には @joroya/renderer-svg を使用します — ドキュメント、アイコン、静的イラストに最適です。

import { renderToSVG } from '@joroya/renderer-svg';

const svgString = renderToSVG(scene, {
  width: 200,
  height: 200,
  viewBox: '0 0 200 200', // オプション
});

document.getElementById('svg-container')!.innerHTML = svgString;

Canvas 2D

シェイプ、テキスト、パスを含む2Dラスターレンダリングには @joroya/renderer-canvas2d を使用します。

import { renderToCanvas } from '@joroya/renderer-canvas2d';

renderToCanvas(scene, canvas, {
  width: 500,
  height: 500,
  backgroundColor: { r: 0.06, g: 0.07, b: 0.1 },
});

シーン階層の例

変換はツリーを通じて伝播します。子のワールド行列は、親のワールド行列と自身のローカル行列の積です。

const solarSystem = new Node('solar-system');

// 原点にある太陽
const sun = new Node('sun');
sun.addComponent(createSphere(2, 32, 32));
sun.addComponent(new Material({
  color: { r: 1, g: 0.9, b: 0.2 },
  emissive: { r: 1, g: 0.7, b: 0.1 },
}));
solarSystem.add(sun);

// 地球は太陽の周りを公転
const earth = new Node('earth');
earth.addComponent(createSphere(0.5, 32, 32));
earth.addComponent(new Material({ color: { r: 0.2, g: 0.5, b: 1.0 } }));
earth.transform.position.x = 5;
solarSystem.add(earth);

// 月は地球の周りを公転
const moon = new Node('moon');
moon.addComponent(createSphere(0.15, 16, 16));
moon.addComponent(new Material({ color: { r: 0.8, g: 0.8, b: 0.8 } }));
moon.transform.position.x = 1;
earth.add(moon);

scene.add(solarSystem);

// アニメーションループでシステムを回転
function animate(time: number) {
  time *= 0.001;
  requestAnimationFrame(animate);

  solarSystem.transform.rotation.y = time * 0.1;
  solarSystem.transform.updateLocalMatrix();

  earth.transform.rotation.y = time * 0.5;
  earth.transform.updateLocalMatrix();

  renderer.render();
}
requestAnimationFrame(animate);

glTFモデルの読み込み

@joroya/loader-gltf パッケージを使用すると、標準的なglTF/GLBファイルを読み込んでOroyaのシーンノードに変換できます。

npm install @joroya/loader-gltf three
import { loadGLTF } from '@joroya/loader-gltf';
import { ThreeRenderer } from '@joroya/renderer-three';

const { scene: gltfScene, animations } = await loadGLTF('/models/robot.glb');

const renderer = new ThreeRenderer({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: 800,
  height: 600,
});
renderer.mount(gltfScene);

// アニメーションがあれば再生
if (animations.length > 0) {
  const mixer = new AnimationMixer(gltfScene);
  mixer.play(animations[0]);
}

function loop() {
  renderer.render();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

シリアライゼーション

OroyaシーンはJSONにシリアライズし、デシリアライズして元に戻すことができます。シーンの保存/読み込みやネットワーク経由での送信が容易になります。

import { serialize, deserialize } from '@joroya/core';

// シーンをJSONに保存
const json = serialize(scene);
localStorage.setItem('my-scene', json);

// JSONからシーンを読み込み
const restored = deserialize(localStorage.getItem('my-scene')!);
renderer.mount(restored);

React統合

Oroyaをreactコンポーネントでラップして、UIに統合できます:

import { useEffect, useRef } from 'react';
import {
  Scene, Node, createBox, Material,
  Camera, CameraType, Light, LightType,
} from '@joroya/core';
import { ThreeRenderer } from '@joroya/renderer-three';

function OroyaCanvas({ scene }: { scene: Scene }) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const renderer = new ThreeRenderer({
      canvas,
      width: canvas.clientWidth,
      height: canvas.clientHeight,
    });
    renderer.mount(scene);

    let frameId: number;
    const animate = (time: number) => {
      renderer.render();
      frameId = requestAnimationFrame(animate);
    };
    frameId = requestAnimationFrame(animate);

    return () => {
      cancelAnimationFrame(frameId);
      renderer.dispose();
    };
  }, [scene]);

  return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />;
}

// 使用例
function App() {
  const scene = new Scene();

  const cam = new Node('cam');
  cam.addComponent(new Camera({
    type: CameraType.Perspective, fov: 75,
    aspect: 16 / 9, near: 0.1, far: 1000,
  }));
  cam.transform.position.z = 5;
  scene.add(cam);

  // ライティング
  const ambient = new Node('ambient');
  ambient.addComponent(new Light({ type: LightType.Ambient, intensity: 0.4 }));
  scene.add(ambient);

  const cube = new Node('cube');
  cube.addComponent(createBox(1, 1, 1));
  cube.addComponent(new Material({ color: { r: 0.1, g: 0.6, b: 0.9 } }));
  scene.add(cube);

  return <OroyaCanvas scene={scene} />;
}

Transformの詳細

すべての Node は3つのプロパティを持つ Transform コンポーネントを持ちます:

プロパティデフォルト値説明
positionVec3 { x, y, z }{ 0, 0, 0 }3D空間での移動
rotationQuat { x, y, z, w }{ 0, 0, 0, 1 }クォータニオンとしての回転
scaleVec3 { x, y, z }{ 1, 1, 1 }スケール係数

重要: transformプロパティを変更した後は、ローカル行列を再計算するために updateLocalMatrix() を呼び出してください。レンダラーは毎フレーム scene.updateWorldMatrices() を呼び出して、階層全体に変換を伝播します。

const node = new Node('box');
node.transform.position = { x: 2, y: 0, z: -3 };
node.transform.scale = { x: 2, y: 2, z: 2 };
node.transform.updateLocalMatrix();

トラバーサルと検索

Oroyaはシーングラフをナビゲートする複数の方法を提供しています:

// すべてのノードを深さ優先で走査
scene.traverse((node) => {
  console.log(node.name, node.id);
});

// 特定のノードを検索
const cam = scene.findNodeByName('main-camera');
const node = scene.findNodeById('some-uuid');

// 階層へのアクセス
const parent = node.parent;           // Node | null
const children = node.children;       // Node[]

次のステップ