🧠 توسعه‌دهندگان Three.js: چرا باید Needle Engine را بشناسید (حتی اگر از آن استفاده نکنید)

اگر با Three.js کار می‌کنید، احتمالاً از کنترل کامل روی صحنه، حلقه رندر، متریال‌ها و تعامل‌ها لذت می‌برید. من مدت زیادی است عمیقاً با Three.js کار می‌کنم، به‌خصوص روی کانفیگوراتورهای سه‌بعدی و ابزارهای تعاملی، و راستش هیچ‌وقت حس نکردم به موتور دیگری نیاز دارم — تا وقتی که Needle Engine را بررسی کردم.

این نوشته درباره جایگزین کردن Three.js نیست. درباره این است که Needle Engine واقعاً چه مشکلی را حل می‌کند و چرا خیلی از توسعه‌دهندگان Three.js آن را اشتباه می‌فهمند.

❌ یک سوءبرداشت رایج

بیشتر توسعه‌دهندگان Three.js فکر می‌کنند: «Needle Engine فقط یک انتزاع دیگر است که Three.js را پنهان می‌کند و کنترل را محدود می‌کند.» این تا حدی درست است، اما نکته اصلی را از دست می‌دهد. Needle Engine نمی‌خواهد جای Three.js را بگیرد؛ می‌خواهد مشکلات معماری و روند کاری را حل کند، به‌خصوص در پروژه‌های بزرگ یا پرمحتوا.

🧩 Needle Engine واقعاً چیست

  • Three.js ← رندر و کنترل سطح پایین
  • Needle ← ساختار، چرخه عمر و روند کاری

زیر پوسته هنوز از Three.js استفاده می‌کند: مش‌ها همچنان THREE.Mesh و متریال‌ها همچنان THREE.Material هستند. چیزی که تغییر می‌کند شیوه سازمان‌دهی منطق است.

🧠 ایده اصلی: معماری مبتنی بر Behaviour

در Three.js خالص، منطق اغلب اینجا جمع می‌شود:

function animate() {
  rotateMesh();
  handleSelection();
  updateMaterials();
}

با بزرگ شدن پروژه، نگهداری، گسترش و فهم این کد سخت می‌شود. Needle Engine Behaviourها را معرفی می‌کند: واحدهای منطق قابل استفاده دوباره و قابل اتصال که به یک آبجکت تعلق دارند. به جای «یک تابع را صدا بزن تا تکسچر عوض شود»، فکر می‌کنید «این آبجکت توانایی عوض کردن تکسچرش را دارد».

🎯 مثال واقعی: کانفیگوراتور سه‌بعدی کابینت

کابینتی با بدنه، در، رویه و دستگیره را تصور کنید. در یک پیاده‌سازی معمول Three.js، Raycast می‌کنید، مش انتخاب‌شده را سراسری نگه می‌دارید و تکسچرها را با شرط و switch اعمال می‌کنید. با ذهنیت Needle:

Cabinet Part (GameObject)
├─ Selectable
├─ SelectionVisual
└─ MaterialSlot

منطق انتخاب روی خود آبجکت است، منطق متریال روی خود آبجکت است و رابط کاربری هیچ چیزی درباره مش‌ها یا متریال‌ها نمی‌داند. رابط کاربری فقط این کار را می‌کند:

selected.getComponent(MaterialSlot).setTexture(texture);

نه if، نه switch، نه منطق سراسری درهم‌برهم.

⚖️ تابع در برابر Behaviour

تابع یک کار یک‌باره است، بدون چرخه عمر، بدون مالکیت و بدون state. Behaviour هویت و چرخه عمر دارد (start، update، onDestroy)، به یک آبجکت مشخص تعلق دارد، می‌تواند فعال یا غیرفعال شود و با پیچیدگی پروژه مقیاس می‌گیرد. این تفاوت در کانفیگوراتورها، تجربه‌های AR/WebXR و محصولات سه‌بعدی ماژولار خیلی بزرگ می‌شود.

🧩 چرا خیلی از توسعه‌دهندگان Three.js Needle را «نمی‌فهمند»

چون Needle Engine بعد از بزرگ شدن پروژه می‌درخشد. برای یک دموی تک‌صحنه یا یک آبجکت تعاملی کوچک، Three.js خالص اغلب انتخاب بهتری است. اما برای تجربه‌های پرمحتوا، روند کاری مشترک هنرمند و توسعه‌دهنده، صحنه‌های تعاملی بزرگ و محصولات آماده AR، ساختار Needle خیلی منطقی می‌شود.

💡 مهم‌ترین نکته

حتی اگر هیچ‌وقت مستقیماً از Needle Engine استفاده نکنید، روش بهتری برای ساختاردهی پروژه‌های Three.js یادتان می‌دهد. می‌توانید تفکر مبتنی بر Behaviour، منطق مبتنی بر مالکیت و جداسازی چرخه عمر را بردارید و در Three.js خالص به کار ببرید. همین به‌تنهایی ارزش یادگیری دارد.

تابع در برابر کامپوننت — یک مثال کاربردی

در خیلی از پروژه‌های Three.js، انتخاب و تغییر تکسچر با توابع و state سراسری انجام می‌شود. برای دموهای کوچک کار می‌کند اما زود از کنترل خارج می‌شود.

❌ رویکرد سنتی مبتنی بر تابع

let selectedMesh = null;

function select(mesh) {
  selectedMesh = mesh;
}

function applyTexture(texture) {
  if (!selectedMesh) return;
  selectedMesh.material.map = texture;
  selectedMesh.material.needsUpdate = true;
}

این رویکرد به state سراسری، وابستگی تنگاتنگ و منطقی بیرون از آبجکت تکیه دارد.

✅ رویکرد مبتنی بر کامپوننت (Needle Engine)

انتخاب یک کامپوننت است:

import { Behaviour, PointerEventData } from "@needle-tools/engine";
import { SelectionManager } from "./SelectionManager.js";

export class Selectable extends Behaviour {
  onPointerClick(_args: PointerEventData) {
    // Update the global selection
    SelectionManager.current = this.gameObject;
  }
}

مدیریت متریال کامپوننت دیگری است:

import { Behaviour, Object3D } from "@needle-tools/engine";
import * as THREE from "three";

export class MaterialSlot extends Behaviour {
  @Object3D()
  mesh!: THREE.Mesh;

  setTextureColor(color: number) {
    const mat = this.mesh.material as THREE.MeshStandardMaterial;
    mat.color.set(color);
    mat.needsUpdate = true;
  }
}
import { Behaviour } from "@needle-tools/engine";

export class SelectionManager extends Behaviour {
  static current: any = null;
}

کد رابط کاربری کاملاً عمومی می‌ماند:

import { onStart, addComponent } from "@needle-tools/engine";
import * as THREE from "three";
import { Selectable } from "./Selectable.js";
import { MaterialSlot } from "./MaterialSlot.js";
import { SelectionManager } from "./SelectionManager.js";

onStart(context => {
  const scene = context.scene;
  context.mainCamera.position.set(0, 1, 5);

  // Add a SelectionManager to the scene
  addComponent(scene, new SelectionManager());

  // Create objects
  const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0xaaaaaa })
  );
  cube.position.set(-1, 0.5, 0);
  scene.add(cube);

  const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 32, 32),
    new THREE.MeshStandardMaterial({ color: 0xff0000 })
  );
  sphere.position.set(1, 0.5, 0);
  scene.add(sphere);

  // Attach components
  addComponent(cube, new Selectable());
  addComponent(cube, new MaterialSlot());
  addComponent(sphere, new Selectable());
  addComponent(sphere, new MaterialSlot());

  // Create the UI
  const menu = document.createElement("div");
  document.body.appendChild(menu);

  [0xff0000, 0x00ff00, 0x0000ff, 0xffff00].forEach((color, index) => {
    const btn = document.createElement("button");
    btn.textContent = `Color ${index + 1}`;
    btn.style.backgroundColor = `#${color.toString(16)}`;
    btn.addEventListener("click", () => {
      const selected = SelectionManager.current;
      if (!selected) return;
      selected.getComponent(MaterialSlot)?.setTextureColor(color);
    });
    menu.appendChild(btn);
  });
});

چرا این مهم است

  • منطق انتخاب چیزی درباره متریال‌ها نمی‌داند.
  • منطق تکسچر چیزی درباره ورودی یا Raycasting نمی‌داند.
  • رابط کاربری چیزی درباره مش‌ها یا Three.js نمی‌داند.

هر آبجکت توانایی‌های خودش را دارد و به توابع سراسری وابسته نیست. ایده اصلی Needle Engine همین است: ساختار و مالکیت، نه فقط رندر.

🧭 جمع‌بندی

Needle Engine «بهتر از Three.js» یا «جایگزین Three.js» نیست. لایه‌ای متفاوت از تفکر است — و برای توسعه‌دهندگان باتجربه Three.js، فهم این لایه می‌تواند شیوه طراحی اپلیکیشن‌های سه‌بعدی پیچیده را به‌طور چشمگیری بهتر کند.