Skip to content

Commit

Permalink
feat: readme
Browse files Browse the repository at this point in the history
  • Loading branch information
giulioz committed Jun 6, 2023
1 parent 2c6cef8 commit 67c6b2a
Show file tree
Hide file tree
Showing 11 changed files with 5,424 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.x'
node-version: '18.x'
- name: Install deps
# this runs a build script so there is no dedicated build
run: yarn install
Expand All @@ -39,7 +39,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.x'
node-version: '18.x'
- name: Install deps
# this runs a build script so there is no dedicated build
run: yarn install
Expand Down
5,168 changes: 5,168 additions & 0 deletions .storybook/public/robotoRegular3D/Roboto_Regular.json

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions .storybook/stories/Demo3D.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import * as React from 'react'
import { withKnobs } from '@storybook/addon-knobs'
import { Canvas, useFrame } from '@react-three/fiber'
import { OrbitControls, Text3D } from '@react-three/drei'
import { Perf } from 'r3f-perf/dist/components/Perf.js'

import { InstancedTextProvider, InstancedText } from '../../src'
// @ts-ignore
import robotoRegular3DInfo from '../public/robotoRegular3D/info.json'
import { useRef } from 'react'
import { Group, Mesh } from 'three'
import { TextAlignment } from '../../src/text_engine_3d/types'
import { TextGeometry, FontLoader } from 'three-stdlib'
import { suspend } from 'suspend-react'

export default {
title: 'Demo3D',
decorators: [
withKnobs,
(storyFn) => (
<Canvas>
{storyFn()}
<OrbitControls />
<Perf logsPerSecond={1} />
</Canvas>
),
],
}

const styles = [
{
fontMetadata: robotoRegular3DInfo,
offsetsPath: '/robotoRegular3D/font.png',
normalsPath: '/robotoRegular3D/normals.png',
},
]

function Demo3DScene() {
const width = 32
const height = 128
const ratio = width / height

const groupRef = useRef<Group>(null)

useFrame(({ clock }) => {
groupRef.current?.children.forEach((c) => {
const px = (c.position.x / 6 - width / 2) / ratio
const pz = c.position.z / 1.8 - height / 2
const dist = Math.sqrt(px ** 2 + pz ** 2)
const py = Math.sin(dist / 3 + clock.elapsedTime) * 3
c.position.y = py
;(c as any).textAPI.setGlyphs(py.toFixed(4))
})
})

return (
<InstancedTextProvider styles={styles}>
<group ref={groupRef}>
{new Array(width * height).fill(0).map((_, i) => (
<InstancedText
key={`${i % width},${Math.floor(i / width)}`}
position={[(i % width) * 6, 0, Math.floor(i / width) * 1.8]}
rotation={[-Math.PI / 2, 0, 0]}
text={`AAAAA`}
font={styles[0]}
thickness={0.5}
fontSize={1}
textAlign={TextAlignment.center}
/>
))}
</group>

<pointLight position={[(width / 2) * 6, 30, (height / 2) * 1.8]} color="white" />
<pointLight position={[(width / 2) * 6, 30, 0]} color="red" />
<pointLight position={[0, 30, (height / 2) * 1.8]} color="blue" />
<pointLight position={[(width / 2) * 6, 30, height * 1.8]} color="green" />
</InstancedTextProvider>
)
}

export const Demo3DSt = () => <Demo3DScene />
Demo3DSt.storyName = 'Default'

function Demo3DNoInstancingScene() {
const width = 8
const height = 32
const ratio = width / height

const groupRef = useRef<Group>(null)

const font = suspend(async () => {
let data = await (await fetch('/robotoRegular3D/Roboto_Regular.json' as string)).json()
let loader = new FontLoader()
return loader.parse(data as any)
}, [])

useFrame(({ clock }) => {
groupRef.current?.children.forEach((c) => {
const px = (c.position.x / 6 - width / 2) / ratio
const pz = c.position.z / 1.8 - height / 2
const dist = Math.sqrt(px ** 2 + pz ** 2)
const py = Math.sin(dist / 3 + clock.elapsedTime) * 3
c.position.y = py

const prevGeom = (c.children[0] as Mesh).geometry as any
;((c.children[0] as Mesh).geometry as TextGeometry) = new TextGeometry(py.toFixed(4), {
font,
letterSpacing: 0,
lineHeight: 1,
size: 1,
height: 0.2,
bevelThickness: 0.1,
bevelSize: 0.01,
bevelEnabled: false,
bevelOffset: 0,
curveSegments: 8,
})
prevGeom.dispose()
})
})

return (
<>
<group ref={groupRef}>
{new Array(width * height).fill(0).map((_, i) => (
<group
key={`${i % width},${Math.floor(i / width)}`}
position={[(i % width) * 6, 0, Math.floor(i / width) * 1.8]}
rotation={[-Math.PI / 2, 0, 0]}
>
<Text3D font="/robotoRegular3D/Roboto_Regular.json" frustumCulled={false}>
AAAAA
<meshPhongMaterial />
</Text3D>
</group>
))}
</group>

<pointLight position={[(width / 2) * 6, 30, (height / 2) * 1.8]} color="white" />
<pointLight position={[(width / 2) * 6, 30, 0]} color="red" />
<pointLight position={[0, 30, (height / 2) * 1.8]} color="blue" />
<pointLight position={[(width / 2) * 6, 30, height * 1.8]} color="green" />
</>
)
}

export const Demo3DNoInstancingSt = () => <Demo3DNoInstancingScene />
Demo3DNoInstancingSt.storyName = 'No Instancing'
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ npm install thomas

Try our [demo here](https://thomas-the-text-engine.netlify.app)!

## API for 2D Text Rendering
## API for 2D Text

thomas provides high quality instanced rendering for 2D quads with text, using multichannel signed distance fields.

Expand All @@ -19,6 +19,7 @@ thomas provides high quality instanced rendering for 2D quads with text, using m
<img src="docs/colors-2d.png" width="250px" />
<img src="docs/align-2d.png" width="250px" />
</div>
<br />

```tsx
// Provides the paths to the font to the provider, should be served by your webserver
Expand Down Expand Up @@ -76,6 +77,53 @@ function Text2D() {
}
```

## API for 3D Text

Like for the 2D text, wrap your scene with a provider and spawn instances using the `<InstancedText />` component.

<div>
<img src="docs/persp-3d.png" width="250px" />
<img src="docs/demo-3d.png" width="250px" />
</div>
<br />

```tsx
import robotoRegular3DInfo from '../public/robotoRegular3D/info.json'

// Provides the paths to the font to the provider, should be served by your webserver
const styles = [
{
fontMetadata: robotoRegular3DInfo,
offsetsPath: '/robotoRegular3D/font.png',
normalsPath: '/robotoRegular3D/normals.png',
},
]

function Text3DScene() {
return (
<InstancedTextProvider styles={styles}>
{/* InstancedText will get the transforms from the parent */}
<group position={[10, 0, 0]}>
<InstancedText
text="Example text"
font={styles[0]}
// Depth of the text geometry
thickness={1}
// Size of the text geometry
fontSize={1}
// Optional props
textAlign={TextAlignment.left}
color={new Color('#ff0000')}
opacity={0.8}

// Also accepts any Object3D props
/>
</group>
</InstancedTextProvider>
)
}
```

## Acknowledgements

- [drei](https://github.com/pmndrs/drei) for the library setup
Expand Down
Binary file added docs/demo-3d.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/persp-3d.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 26 additions & 4 deletions src/text_engine_3d/InstancedText.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GroupProps } from '@react-three/fiber'
import * as React from 'react'
import { useEffect, useLayoutEffect, useRef } from 'react'
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
import { Box3, Color, Sphere, Vector3 } from 'three'

import { TextInstancesPtr } from './instancedTextBuffers'
Expand Down Expand Up @@ -130,9 +130,31 @@ export function InstancedText({
}
}, [api, opacity])

const textAPI = useMemo(
() => ({
getBuffers: () => myPtrRef.current && api.getBuffers(myPtrRef.current),
getPtr: () => myPtrRef.current,
setGlyphs: (text: string) => {
if (!myPtrRef.current) return
const buffers = api.getBuffers(myPtrRef.current)
if (!buffers) return

const glyphIds = Array.from(text).map((c) => font.fontMetadata.charToIndex[c])
buffers.instanceGIndexBuffer.set(glyphIds, myPtrRef.current.start)
buffers.instanceGIndexBufferAttribute.needsUpdate = true
},
}),
[font.fontMetadata.charToIndex]
)

return (
<group scale={[fontSize * (scale?.x ?? 1), fontSize * (scale?.y ?? 1), thickness * (scale?.z ?? 1)]} {...rest}>
<instancedTextPlaceholder ref={groupRef} />
</group>
// @ts-ignore
<instancedTextPlaceholder
scale={[fontSize * (scale?.x ?? 1), fontSize * (scale?.y ?? 1), thickness * (scale?.z ?? 1)]}
{...rest}
textAPI={textAPI}
ref={groupRef}
frustumCulled={false}
/>
)
}
5 changes: 4 additions & 1 deletion src/text_engine_3d/InstancedTextPlaceholder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactThreeFiber, extend } from '@react-three/fiber'
import { Box3, Group, Intersection, Matrix4, Ray, Raycaster, Sphere, Vector3 } from 'three'
import { InstancedTextAPI } from './types'

export function registerInstancedTextPlaceholderToR3F() {
extend({ InstancedTextPlaceholder })
Expand Down Expand Up @@ -32,8 +33,10 @@ const _sphere = new Sphere()
const _vA = new Vector3()

export class InstancedTextPlaceholder extends Group {
textAPI?: InstancedTextAPI = undefined

// For correct bounding box
geometry = new InstancedTextPlaceholderGeometry()
geometry: InstancedTextPlaceholderGeometry = new InstancedTextPlaceholderGeometry()

override raycast(raycaster: Raycaster, intersects: Intersection[]) {
const geometry = this.geometry
Expand Down
10 changes: 9 additions & 1 deletion src/text_engine_3d/InstancedTextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface IInstancedTextAPI {
) => TextInstancesPtr | undefined
removeInstances: (ptr: TextInstancesPtr) => void
setOpacity: (ptr: TextInstancesPtr, opacity: number) => void
getBuffers: (ptr: TextInstancesPtr) => InstancedTextBuffers | null
}

const instancedTextContext = createContext<IInstancedTextAPI | null>(null)
Expand Down Expand Up @@ -122,6 +123,9 @@ const StyleGroup = forwardRef<IInstancedTextAPI & { font: Font3D }, { font: Font
}
buffers.instanceColorBufferAttribute.needsUpdate = true
},
getBuffers: () => {
return buffersRef.current
},
}),
[font, realloc]
)
Expand All @@ -134,7 +138,7 @@ const StyleGroup = forwardRef<IInstancedTextAPI & { font: Font3D }, { font: Font
})

if (!geometry) return null
return <instancedMesh args={[geometry, textMat, instanceAllocated]} />
return <instancedMesh frustumCulled={false} args={[geometry, textMat, instanceAllocated]} />
})

export function InstancedTextProvider({ children, styles }: PropsWithChildren<{ styles: Font3D[] }>) {
Expand All @@ -161,6 +165,10 @@ export function InstancedTextProvider({ children, styles }: PropsWithChildren<{
const api = stylesApisRef.current.get(ptr.font)
api?.setOpacity(ptr, opacity)
},
getBuffers: (ptr) => {
const api = stylesApisRef.current.get(ptr.font)
return api?.getBuffers(ptr) ?? null
},
}),
[]
)
Expand Down
8 changes: 8 additions & 0 deletions src/text_engine_3d/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { InstancedTextBuffers, TextInstancesPtr } from './instancedTextBuffers'

export enum TextAlignment {
left = 'left',
right = 'right',
Expand Down Expand Up @@ -31,3 +33,9 @@ interface BoundingBox {
xMax: number
yMax: number
}

export interface InstancedTextAPI {
getBuffers: () => InstancedTextBuffers | null
getPtr: () => TextInstancesPtr | null
setGlyphs: (text: string) => void
}
13 changes: 10 additions & 3 deletions src/text_engine_3d/useInstancedTextMaterial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import { useTexture } from '@react-three/drei'

import { FontInfo3D } from './types'

export function useInstancedTextMaterial(offsetsTexture: Texture, normalsTexture: Texture, fontMetadata: FontInfo3D) {
const defaultMaterial = () => new MeshPhongMaterial()

export function useInstancedTextMaterial(
offsetsTexture: Texture,
normalsTexture: Texture,
fontMetadata: FontInfo3D,
originalMaterial = defaultMaterial
) {
const textMat = useMemo(() => {
const material = new MeshPhongMaterial()
const material = originalMaterial()
material.onBeforeCompile = (shader) => {
shader.uniforms = {
...shader.uniforms,
Expand Down Expand Up @@ -62,7 +69,7 @@ export function useInstancedTextMaterial(offsetsTexture: Texture, normalsTexture
}

return material
}, [offsetsTexture, normalsTexture, fontMetadata])
}, [offsetsTexture, normalsTexture, fontMetadata, originalMaterial])

return textMat
}
Expand Down

0 comments on commit 67c6b2a

Please sign in to comment.