Skip to content

Commit

Permalink
add example and guide for three + pixi
Browse files Browse the repository at this point in the history
  • Loading branch information
GoodBoyDigital committed Dec 9, 2024
1 parent 10fe68e commit 2959b5b
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 2 deletions.
151 changes: 151 additions & 0 deletions docs/guides/advanced/mixing-three-and-pixi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Example from '@site/src/components/Example/index';
import version from '@site/docs/pixi-version.json';

# Mixing PixiJS and Three.js

In many projects, developers aim to harness the strengths of both 3D and 2D graphics. Combining the advanced 3D rendering capabilities of Three.js with the speed and versatility of PixiJS for 2D can result in a powerful, seamless experience. Together, these technologies create opportunities for dynamic and visually compelling applications. Lets see how to do this.

:::info NOTE
This guide assumes PixiJS will be used as the top layer to deliver UI over a 3D scene rendered by Three.js. However, developers can render either in any order, as many times as needed. This flexibility allows for creative and dynamic applications.
:::

---

### What You’ll Learn

- Setting up PixiJS and Three.js to share a single WebGL context.
- Using `resetState` to manage renderer states.
- Avoiding common pitfalls when working with multiple renderers.

---

### Setting Up

#### Step 1: Initialize Three.js Renderer and Scene

Three.js will handle the 3D rendering the creation of the dom element and context.

```javascript
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

const threeRenderer = new THREE.WebGLRenderer({
antialias: true,
stencil: true // so masks work in pixijs
});

threeRenderer.setSize(WIDTH, HEIGHT);
threeRenderer.setClearColor(0xdddddd, 1);
document.body.appendChild(threeRenderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, WIDTH / HEIGHT);
camera.position.z = 50;
scene.add(camera);

const boxGeometry = new THREE.BoxGeometry(10, 10, 10);
const basicMaterial = new THREE.MeshBasicMaterial({ color: 0x0095dd });
const cube = new THREE.Mesh(boxGeometry, basicMaterial);
cube.rotation.set(0.4, 0.2, 0);
scene.add(cube);
```
:::info NOTE
We used the dom element and context created by the three.js renderer to pass to the pixijs renderer.
This was the simplest way to ensure that the two renderers were using the same WebGL context. You could have done it the other way round
if you wanted to.
:::

#### Step 2: Initialize PixiJS Renderer and Stage

PixiJS will handle the 2D overlay.

```javascript
const pixiRenderer = new PIXI.WebGLRenderer();

await pixiRenderer.init({
context: threeRenderer.getContext(),
width: WIDTH,
height: HEIGHT,
clearBeforeRender: false, // Prevent PixiJS from clearing the Three.js render
});

const stage = new PIXI.Container();
const amazingUI = new PIXI.Graphics()
.roundRect(20, 80, 100, 100, 5)
.roundRect(220, 80, 100, 100, 5)
.fill(0xffff00);

stage.addChild(amazingUI);
```

---

### Rendering Loop

To ensure smooth transitions between the renderers, reset their states before each render:

```javascript
function render() {
// Render the Three.js scene
threeRenderer.resetState();
threeRenderer.render(scene, camera);

// Render the PixiJS stage
pixiRenderer.resetState();
pixiRenderer.render({ container: stage });

requestAnimationFrame(render);
}

requestAnimationFrame(render);

```

---

### Example: Combining 3D and 2D Elements

Here’s the complete example integrating PixiJS and Three.js:

<Example id="advanced.threeAndPixi" pixiVersion={version} mode={"embedded"} />

---

### Gotchas

- **Enable Stencil Buffers:**

- When creating the Three.js renderer, ensure `stencil` is set to `true`. This allows PixiJS masks to work correctly.

- **Keep Dimensions in Sync:**

- Ensure both renderers use the same `width` and `height` to avoid visual mismatches—so be careful when resizing one, you need to resize the other!

- **Pass the WebGL Context:**

- Pass the WebGL context from Three.js to PixiJS during initialization using `pixiRenderer.init({ context: threeRenderer.getContext() });`.

- **Disable Clear Before Render:**

- Set `clearBeforeRender: false` when initializing the PixiJS renderer. This prevents PixiJS from clearing the Three.js content that was rendered before it.
- Alternatively you can set `clear: false` in the `pixiRenderer.render()` call. eg `pixiRenderer.render({ container: stage, clear: false });`.

- **Manage Render Order:**

- In this example, Three.js is rendered first, followed by PixiJS for UI layers. However, this order is flexible. You can render pixi -> three -> pixi is you want, just make sure you reset the state when switching renderer.

- **Separate Resources:**

- Remember that resources like textures are not shared between PixiJS and Three.js. A PixiJS texture cannot be directly used as a Three.js texture and vice versa.

---

### Conclusion

Mixing PixiJS and Three.js can be a powerful way to create dynamic and visually appealing applications. By carefully managing the rendering loop and states, you can achieve seamless transitions between 3D and 2D layers. This approach allows you to leverage the strengths of both technologies, creating applications that are both visually stunning and performant.

This technique can be used with other renderers too - as long as they have their own way of resetting their state (which the main ones do) you can mix them. Popular 3D engines like Babylon.js and PlayCanvas both support state management through their respective APIs, making them compatible with this mixing approach. This gives you the flexibility to choose the 3D engine that best suits your needs while still leveraging PixiJS's powerful 2D capabilities.




6 changes: 5 additions & 1 deletion sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ const sidebars = {
type: 'category',
label: 'Advanced',
collapsed: true,
items: ['guides/advanced/render-groups', 'guides/advanced/cache-as-texture'],
items: [
'guides/advanced/render-groups',
'guides/advanced/cache-as-texture',
'guides/advanced/mixing-three-and-pixi',
],
},
{
type: 'category',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export const useSandpackConfiguration = ({
extraPackages,
});

(dependencies as any).three = '^0.171.0';

// TODO: adding code here is only necessary because of user edited code, otherwise we
// could flip between examples easily, investigate why it bugs out during editing
const key = `${dependenciesKey}-${code}-${Object.values(extraFiles ?? {}).join('-')}`;
Expand Down
101 changes: 101 additions & 0 deletions src/examples/v8.0.0/advanced/threeAndPixi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Import required classes from PixiJS and Three.js
import { Container, Graphics, Text, WebGLRenderer } from 'pixi.js';
import * as THREE from 'three';

// Self-executing async function to set up the demo
(async () =>
{
// Initialize window dimensions
let WIDTH = window.innerWidth;
let HEIGHT = window.innerHeight;

// === THREE.JS SETUP ===
// Create Three.js WebGL renderer with antialiasing and stencil buffer
const threeRenderer = new THREE.WebGLRenderer({ antialias: true, stencil: true });

// Configure Three.js renderer size and background color
threeRenderer.setSize(WIDTH, HEIGHT);
threeRenderer.setClearColor(0xdddddd, 1); // Light gray background
document.body.appendChild(threeRenderer.domElement);

// Create Three.js scene
const scene = new THREE.Scene();

// Set up perspective camera with 70° FOV
const threeCamera = new THREE.PerspectiveCamera(70, WIDTH / HEIGHT);

threeCamera.position.z = 50; // Move camera back to see the scene
scene.add(threeCamera);

// Create a simple cube mesh
const boxGeometry = new THREE.BoxGeometry(30, 30, 30);
const basicMaterial = new THREE.MeshBasicMaterial({ color: 0x0095dd }); // Blue color
const cube = new THREE.Mesh(boxGeometry, basicMaterial);

scene.add(cube);

// === PIXI.JS SETUP ===
// Create PixiJS renderer that shares the WebGL context with Three.js
const pixiRenderer = new WebGLRenderer();

// Initialize PixiJS renderer with shared context
await pixiRenderer.init({
context: threeRenderer.getContext(),
width: WIDTH,
height: HEIGHT,
clearBeforeRender: false, // Don't clear the canvas as Three.js will handle that
});

// Create PixiJS scene graph
const stage = new Container();

// Create a yellow rounded rectangle UI element
const uiLayer = new Graphics().roundRect(20, 80, 300, 300, 20).fill(0xffff00);

// Add text overlay
const text = new Text({ text: 'Pixi and Three.js', style: { fontFamily: 'Arial', fontSize: 24, fill: 'black' } });

uiLayer.addChild(text);
stage.addChild(uiLayer);

// Animation loop
function loop()
{
// Rotate cube continuously
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;

// Animate UI layer position using sine wave
uiLayer.y = ((Math.sin(Date.now() * 0.001) + 1) * 0.5 * WIDTH) / 2;

// Render Three.js scene
threeRenderer.resetState();
threeRenderer.render(scene, threeCamera);

// Render PixiJS scene
pixiRenderer.resetState();
pixiRenderer.render({ container: stage });

// Continue animation loop
requestAnimationFrame(loop);
}

// Start animation loop
requestAnimationFrame(loop);

// Handle window resizing
window.addEventListener('resize', () =>
{
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;

// Update Three.js renderer
threeRenderer.setSize(WIDTH, HEIGHT);
// Update Three.js camera aspect ratio so it renders correctly
threeCamera.aspect = WIDTH / HEIGHT;
threeCamera.updateProjectionMatrix();

// Update PixiJS renderer
pixiRenderer.resize(WIDTH, HEIGHT);
});
})();
3 changes: 2 additions & 1 deletion src/examples/v8.0.0/examplesData.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"mouseTrail",
"screenShot",
"collisionDetection",
"spinners"
"spinners",
"threeAndPixi"
],
"sprite": [
"basic",
Expand Down
2 changes: 2 additions & 0 deletions src/examples/v8.0.0/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import spinnerGenerator3 from '!!raw-loader!./advanced/spinners/spinner3.js';
import spinnerGenerator4 from '!!raw-loader!./advanced/spinners/spinner4.js';
import spinnerGenerator5 from '!!raw-loader!./advanced/spinners/spinner5.js';
import starWarp from '!!raw-loader!./advanced/starWarp.js';
import threeAndPixi from '!!raw-loader!./advanced/threeAndPixi.js';

import async from '!!raw-loader!./assets/async.js';
import background from '!!raw-loader!./assets/background.js';
Expand Down Expand Up @@ -230,6 +231,7 @@ const examplesSource: ExamplesSourceType = {
'src/intersect.js': spinnersIntersect,
},
starWarp,
threeAndPixi,
},
meshAndShaders: {
perspectiveMesh,
Expand Down

0 comments on commit 2959b5b

Please sign in to comment.