Skip to content
This repository has been archived by the owner on Apr 4, 2022. It is now read-only.

Commit

Permalink
Support for custom renderers (#43, #24)
Browse files Browse the repository at this point in the history
  • Loading branch information
hmans authored Jan 30, 2021
1 parent 7f21df0 commit 3e70f3b
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 66 deletions.
49 changes: 29 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **New:** When assigning to an object property via an attribute, you can now set the attribute to a CSS selector to reference another object. This can, for example, be used to re-use geometries, materials and other potentially expensive resources:

```html
<!-- Resources -->
<three-box-buffer-geometry id="geometry"></three-box-buffer-geometry>
<three-mesh-standard-material id="material" color="#555"></three-mesh-standard-material>
```html
<!-- Resources -->
<three-box-buffer-geometry id="geometry"></three-box-buffer-geometry>
<three-mesh-standard-material id="material" color="#555"></three-mesh-standard-material>

<!-- Scene Contents -->
<three-mesh position="-2, 0, 0" geometry="#geometry" material="#material"></three-mesh>
<three-mesh position="0, 0, 0" geometry="#geometry" material="#material"></three-mesh>
<three-mesh position="2, 0, 0" geometry="#geometry" material="#material"></three-mesh>
```
<!-- Scene Contents -->
<three-mesh position="-2, 0, 0" geometry="#geometry" material="#material"></three-mesh>
<three-mesh position="0, 0, 0" geometry="#geometry" material="#material"></three-mesh>
<three-mesh position="2, 0, 0" geometry="#geometry" material="#material"></three-mesh>
```

- **New:** When working with plain string attributes, you can now use the `deg` suffix to convert the specified value into radians. This is very useful in 100% HTML-based projects where you don't have access to JavaScript's `Math.PI`:

```html
<three-mesh rotation.x="-90deg">...</three-mesh>
```
```html
<three-mesh rotation.x="-90deg">...</three-mesh>
```

- **Changed:** The core ticker loop now makes use of `setAnimationLoop` instead of `requestAnimationFrame`, which is a critical prerequisite for making your three-elements project [WebXR-ready](https://three-elements.hmans.co/advanced/webxr.html).

- **New:** `<three-game>` now can receive an `xr` attribute to enable WebXR features.
- **New:** You can now configure custom renderers! Just like with any other element provided by this library, you can use attributes to configure them to your needs:

```html
<three-game>
<three-web-gl-renderer xr.enabled></three-web-gl-renderer>
<three-scene> ... </three-scene>
</three-game>
```

Renderers that are part of the THREE.\* namespace are supported out of the box; support for SVG and CSS renderers will come in a future update.

- **New:** You no longer have to use valid JSON syntax for `arg` attributes -- just provide a list of comma-separated values:

```html
<three-fog args="#333333, 1, 1000"></three-fog>
```
```html
<three-fog args="#333333, 1, 1000"></three-fog>
```

The commas, in fact, are now purely optional. This will also work:
The commas, in fact, are now purely optional. This will also work:

```html
<three-fog args="#333333 1 1000"></three-fog>
```
```html
<three-fog args="#333333 1 1000"></three-fog>
```

- **Changed:** When attributes on an element map to a non-existing property on the wrapped object, there will no longer be a warning logged to the console. (This is very useful when you're combining three-elements with other frameworks that make use of their own attribute names on your elements.)

Expand Down
4 changes: 3 additions & 1 deletion examples/vr.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
/>
</head>
<body>
<three-game autorender xr id="game">
<three-game autorender id="game">
<three-web-gl-renderer xr.enabled></three-web-gl-renderer>

<three-scene background-color="#ffe" id="mainScene">
<!-- Lights -->
<three-ambient-light intensity="0.2"></three-ambient-light>
Expand Down
11 changes: 2 additions & 9 deletions src/BaseElement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as THREE from "three"
import { ThreeGame, TickerFunction } from "./elements/three-game"
import { ThreeScene } from "./elements/three-scene"
import { IConstructable } from "./types"
Expand Down Expand Up @@ -160,7 +159,7 @@ export class BaseElement extends HTMLElement {
this.debug("connectedCallback")

/* Emit connected event */
this.dispatchEvent(new CustomEvent("connected", { bubbles: true, cancelable: false }))
this.dispatchEvent(new Event("connected", { bubbles: true, cancelable: false }))

/*
Some stuff relies on all custom elements being fully defined and connected. However:
Expand All @@ -180,7 +179,7 @@ export class BaseElement extends HTMLElement {
this.mountedCallback()

/* Emit ready event */
this.dispatchEvent(new CustomEvent("ready", { bubbles: true, cancelable: false }))
this.dispatchEvent(new Event("mounted", { bubbles: true, cancelable: false }))
})
}

Expand All @@ -194,9 +193,6 @@ export class BaseElement extends HTMLElement {
disconnectedCallback() {
this.debug("disconnectedCallback")

/* Emit disconnected event */
this.dispatchEvent(new CustomEvent("disconnected", { bubbles: true, cancelable: false }))

/*
If isConnected is false, this element is being removed entirely. In this case,
we'll do some extra cleanup.
Expand All @@ -209,9 +205,6 @@ export class BaseElement extends HTMLElement {
this.onframetick = undefined
this.onrendertick = undefined

/* Emit disconnected event */
this.dispatchEvent(new CustomEvent("removed", { bubbles: true, cancelable: false }))

/* Invoke removedCallback */
this.removedCallback()
})
Expand Down
2 changes: 1 addition & 1 deletion src/ThreeElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export class ThreeElement<T = any> extends BaseElement {
Create an anonymous class that inherits from our cool base class, but sets
its own Three.js constructor property.
*/
return class extends ThreeElement<T> {
return class extends this<T> {
static threeConstructor = constructor
}
}
Expand Down
117 changes: 91 additions & 26 deletions src/elements/three-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ export type TickerFunction = (dt: number, el: HTMLElement) => any
export class ThreeGame extends HTMLElement {
emitter = new EventEmitter()

renderer = new THREE.WebGLRenderer({
powerPreference: "high-performance",
antialias: true,
stencil: true,
depth: true
})
/* RENDERER */

/** The time delta since the last frame, in fractions of a second. */
deltaTime = 0
get renderer() {
return this._renderer
}

set renderer(v) {
this.cleanupRenderer()
this._renderer = v
this.setupRenderer()
}

protected _renderer: THREE.Renderer = this.makeDefaultRenderer()

/* OPTIMIZED RENDERING */

/** Has a frame been requested to be rendered in the next tick? */
private frameRequested = true
Expand All @@ -30,20 +36,6 @@ export class ThreeGame extends HTMLElement {
}

connectedCallback() {
/* Set up renderer */
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.autoClear = false

/* Configure color space */
this.renderer.outputEncoding = THREE.sRGBEncoding

/* Enable shadow map */
this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap

/* Configure WebXR */
this.renderer.xr.enabled = Boolean(this.hasAttribute("xr"))

/* We'll plug our canvas into the shadow root. */
const shadow = this.attachShadow({ mode: "open" })
shadow.appendChild(this.renderer.domElement)
Expand All @@ -61,8 +53,25 @@ export class ThreeGame extends HTMLElement {
this.handleWindowResize = this.handleWindowResize.bind(this)
window.addEventListener("resize", this.handleWindowResize, false)

/* Initialize window size */
this.handleWindowResize()
/* Initialize renderer size */
this.setupRenderer()

/* Look out for some specific stuff connecting within our branch of the document */
this.addEventListener("connected", (e) => {
const target = e.target as HTMLElement & { object?: any }

if (target) {
/*
Pick up renderers as they connect. We need to figure out if the originating element
represents a Three.js renderer. This is made slightly difficult by renderers not
having a common base class, and no `isRenderer` property being available. Time
to get creative and just make a wild guess. :>
*/
if (target.tagName.endsWith("-RENDERER") && (target as any).object.render) {
this.renderer = target.object
}
}
})

/* Announce that we're ready */
this.dispatchEvent(new Event("ready"))
Expand All @@ -79,9 +88,38 @@ export class ThreeGame extends HTMLElement {
window.removeEventListener("resize", this.handleWindowResize, false)

/* Remove canvas from page */
this.cleanupRenderer()
}

protected setupRenderer() {
this.shadowRoot!.appendChild(this.renderer.domElement)
this.handleWindowResize()
}

protected cleanupRenderer() {
this.shadowRoot!.removeChild(this.renderer.domElement)
}

protected makeDefaultRenderer() {
const renderer = new THREE.WebGLRenderer({
powerPreference: "high-performance",
antialias: true,
stencil: true,
depth: true
})

renderer.autoClear = false

/* Configure color space */
renderer.outputEncoding = THREE.sRGBEncoding

/* Enable shadow map */
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap

return renderer
}

private handleWindowResize() {
/* Get width and height from this very element */
const width = this.clientWidth
Expand All @@ -98,6 +136,14 @@ export class ThreeGame extends HTMLElement {
this.frameRequested = true
}

/* TICKING */

/** Are we currently ticking? */
protected _ticking = false

/** The time delta since the last frame, in fractions of a second. */
deltaTime = 0

startTicking() {
let lastNow = performance.now()

Expand Down Expand Up @@ -129,11 +175,30 @@ export class ThreeGame extends HTMLElement {
}
}

this.renderer.setAnimationLoop(tick)
/*
If we have a WebGLRenderer, we'll use its setAnimationLoop. Otherwise,
we'll perform normal rAF-style ticking.
*/
this._ticking = true

if (this.renderer instanceof THREE.WebGLRenderer) {
this.renderer.setAnimationLoop(tick)
} else {
const loop = () => {
tick()
if (this._ticking) requestAnimationFrame(loop)
}

loop()
}
}

stopTicking() {
this.renderer.setAnimationLoop(null)
this._ticking = false

if (this.renderer instanceof THREE.WebGLRenderer) {
this.renderer.setAnimationLoop(null)
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/elements/three-scene.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Camera, Color, OrthographicCamera, PerspectiveCamera, Scene } from "three"
import { Camera, Color, OrthographicCamera, PerspectiveCamera, Scene, WebGLRenderer } from "three"
import { PointerEvents } from "../PointerEvents"
import { ThreeElement } from "../ThreeElement"
import { getThreeObjectBySelector } from "../util/getThreeObjectBySelector"
Expand Down Expand Up @@ -38,6 +38,8 @@ export class ThreeScene extends ThreeElement.for(Scene) {
}

mountedCallback() {
super.mountedCallback()

/* Set up event processor */
this.pointer = new PointerEvents(this.game.renderer, this.object!, this.camera)

Expand All @@ -57,7 +59,9 @@ export class ThreeScene extends ThreeElement.for(Scene) {
render() {
const { renderer } = this.game

renderer.clearDepth()
if (renderer instanceof WebGLRenderer) {
renderer.clearDepth()
}
renderer.render(this.object!, this.camera)
}

Expand Down
52 changes: 52 additions & 0 deletions test/custom-renderers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect, fixture, html, nextFrame } from "@open-wc/testing"
import * as THREE from "three"
import "../src"

import { ThreeElement, ThreeGame } from "../src"

describe("using custom renderers", () => {
describe("when no custom renderer is configured", () => {
const render = () =>
fixture(html`
<three-game>
<three-scene></three-scene>
</three-game>
`)

it("uses the default renderer", async () => {
const game = (await render()) as ThreeGame
expect(game.renderer).to.be.instanceOf(THREE.WebGLRenderer)
})
})

describe("with a custom renderer configured", () => {
const render = () =>
fixture(html`
<three-game>
<three-web-gl-renderer xr.enabled></three-web-gl-renderer>
<three-scene></three-scene>
</three-game>
`)

it("creates an element that wraps a renderer", async () => {
const game = (await render()) as ThreeGame
const renderer = game.querySelector("three-web-gl-renderer") as ThreeElement

expect(renderer.object).to.be.instanceOf(THREE.WebGLRenderer)
})

it("automatically attaches the custom renderer to the three-game element", async () => {
const game = (await render()) as ThreeGame
const renderer = game.querySelector("three-web-gl-renderer") as ThreeElement

expect(game.renderer).to.eq(renderer.object)
})

it("passes custom options to the renderer", async () => {
const game = (await render()) as ThreeGame
const renderer = game.querySelector("three-web-gl-renderer") as ThreeElement

expect(renderer.object.xr.enabled).to.eq(true)
})
})
})
Loading

0 comments on commit 3e70f3b

Please sign in to comment.