Skip to content

rosskevin/gltfjsx

 
 

Repository files navigation

MIT Version CI PRs Welcome

@rosskevin/gltfjsx

This is intended to be a temporary fork of pmndrs/gltfjsx. I submitted a large PR#286 with this in hopes of having it merged, but the orginal repository appears neglected and I am unsure if it will be reviewed and accepted.

Given the delay, I needed to get published and move forward with our work in using my changes. I still hope this finds a home in the original repository as I appreciate the original authors and contributors.

If my PR is not accepted, I intend to keep this up-to-date for our purposes and accept PRs (that have tests!).

This is an API+CLI tool that turns GLTF assets into declarative and reusable components. At this time we focus on generating react-three-fiber JSX/TSX components, but the API can be used independently to generate code for other frameworks.

The GLTF workflow on the web is not ideal

  • GLTF is thrown whole into the scene which prevents re-use, in threejs objects can only be mounted once
  • Contents can only be found by traversal which is cumbersome and slow
  • Changes to queried nodes are made by mutation, which alters the source data and prevents re-use
  • Re-structuring content, making nodes conditional or adding/removing is cumbersome
  • Model compression is complex and not easily achieved
  • Models often have unnecessary nodes that cause extra work and matrix updates

@rosskevin/gltfjsx fixes that

  • 🧑‍💻 It creates a virtual graph of all objects and materials. Now you can easily alter contents and re-use.
  • 🏎️ The graph gets pruned (empty groups, unnecessary transforms, ...) and will perform better.
  • ⚡️ It will optionally compress your model with up to 70%-90% size reduction.

Demo

gltfjsx-preview.mp4

Usage

Usage
  $ npx @rosskevin/gltfjsx <Model.glb> <options>

Options
    --output, -o        Output src file name/path (default: Model.(j|t)sx)
    --draco, -d         Use draco to load file
    --types, -t         Write as .tsx file with types (default: true)
    --keepnames, -k     Keep original names
    --keepgroups, -K    Keep (empty) groups, disable pruning
    --bones, -b         Lay out bones declaratively (default: false)
    --meta, -m          Include metadata (as userData)
    --shadows, s        Let meshes cast and receive shadows
    --printwidth, w     Prettier printWidth (default: 120)
    --precision, -p     Number of fractional digits (default: 3)
    --root, -r          Sets directory from which .gltf file is served
    --exportdefault, -E Use default export
    --console, -c       Log JSX to console, won't produce a file
    --debug, -D         Debug output
    The following options apply a series of transformations to the GLTF file via the @gltf-transform libraries:
        --instance, -i      Instance re-occuring geometry
        --instanceall, -I   Instance every geometry (for cheaper re-use)
        --resolution, -R  Resolution for texture resizing (default: 1024)
        --keepmeshes, -j  Do not join compatible meshes
        --keepmaterials, -M Do not palette join materials
        --keepattributes, Whether to keep unused vertex attributes, such as UVs without an assigned texture
        --format, -f      Texture format jpeg | png | webp | avif (default: "webp")
        --simplify, -S    Mesh simplification (default: false)
        --ratio         Simplifier ratio (default: 0)
        --error         Simplifier error threshold (default: 0.0001)

A typical use-case

First you run your model through gltfjsx. npx allows you to use npm packages without installing them.

npx @rosskevin/gltfjsx model.gltf --transform

This will create a Model.jsx file that plots out all of the assets contents.

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/

import { useGLTF, PerspectiveCamera } from '@react-three/drei'

const modelLoadPath = '/model-transformed.glb'
export function Model(props) {
  const { nodes, materials } = useGLTF(modelLoadPath)
  return (
    <group {...props} dispose={null}>
      <PerspectiveCamera name="camera" fov={40} near={10} far={1000} position={[10, 0, 50]} />
      <pointLight intensity={10} position={[100, 50, 100]} rotation={[-Math.PI / 2, 0, 0]} />
      <group position={[10, -5, 0]}>
        <mesh geometry={nodes.robot.geometry} material={materials.metal} />
        <mesh geometry={nodes.rocket.geometry} material={materials.wood} />
      </group>
    </group>
  )
}

useGLTF.preload(modelLoadPath)

Add your model to your /public folder as you would normally do. With the --transform flag it has created a compressed copy of it (in the above case model-transformed.glb). Without the flag just copy the original model.

/public
  model-transformed.glb

The component can now be dropped into your scene.

import { Canvas } from '@react-three/fiber'
import { Model } from './Model'

function App() {
  return (
    <Canvas>
      <Model />

You can re-use it, it will re-use geometries and materials out of the box:

<Model position={[0, 0, 0]} />
<Model position={[10, 0, -10]} />

Common manual changes to generated Model.jsx

Change its colors

<mesh geometry={nodes.robot.geometry} material={materials.metal} material-color="green" />

Change materials

<mesh geometry={nodes.robot.geometry}>
  <meshPhysicalMaterial color="hotpink" />
</mesh>

Make contents conditional

{
  condition ? <mesh geometry={nodes.robot.geometry} material={materials.metal} /> : null
}

Add events

<mesh geometry={nodes.robot.geometry} material={materials.metal} onClick={handleClick} />

Features

⚡️ Draco and meshopt compression ootb

You don't need to do anything if your models are draco compressed, simply add the --draco flag.

⚡️ Preload your assets for faster response

The asset will be preloaded by default, this makes it quicker to load and reduces time-to-paint. Remove the preloader if you don't need it.

⚡️ Transform (compression, resize)

With the --transform flag it creates:

  • a binary-packed
  • draco-compressed
  • texture-resized (1024x1024)
  • webp compressed
  • deduped
  • instanced
  • pruned

*.glb ready to be consumed on a web site. It uses glTF-Transform. This can reduce the size of an asset by 70%-90%.

It will not alter the original but create a copy and append [modelname]-transformed.glb.

⚡️ Typescript types

Add the --types flag and your GLTF will be typesafe and generate a .tsx file instead of .jsx with exported named types to match the inferred component name.

interface SpaceGLTF extends GLTF {
  nodes: { robot: Mesh; rocket: Mesh }
  materials: { metal: MeshStandardMaterial; wood: MeshStandardMaterial }
}

export interface SpaceProps extends GroupProps {}

export function Space(props: SpaceProps) {
  const { nodes, materials } = useGLTF<GLTFResult>('/model.gltf')
}

⚡️ Easier access to animations

If your GLTF contains animations it will add drei's useAnimations hook, which extracts all clips and prepares them as actions:

const { nodes, materials, animations } = useGLTF('/model.gltf')
const { actions } = useAnimations(animations, group)

If you want to play an animation you can do so at any time:

<mesh onClick={(e) => actions.jump.play()} />

If you want to blend animations:

const [name, setName] = useState("jump")
...
useEffect(() => {
  actions[name].reset().fadeIn(0.5).play()
  return () => actions[name].fadeOut(0.5)
}, [name])

⚡️ Instancing

Use the --instance flag and it will look for similar geometry and create instances of them. Look into drei/Instances and drei/Merged components to understand how it works. It does not matter if you instanced the model previously in Blender, it creates instances for each mesh that has a specific geometry and/or material.

--instanceall will create instances of all the geometry. This allows you to re-use the model with the smallest amount of drawcalls.

Your export will look like something like this:

const context = createContext()
export function PartsInstances({ children, ...props }) {
  const { nodes } = useGLTF(modelLoadPath, draco) as PartsGLTF
  const instances = useMemo(() => ({ Screw1: nodes['Screw1'], Screw2: nodes['Screw2'] }), [nodes])
  return (
    <Merged meshes={instances} {...props}>
      {(instances) => <context.Provider value={instances} children={children} />}
    </Merged>
  )
}

export function Parts(props) {
  const instances = useContext(context)
  return (
    <group {...props} dispose={null}>
      <instances.Screw1 position={[-0.42, 0.04, -0.08]} rotation={[-Math.PI / 2, 0, 0]} />
      <instances.Screw2 position={[-0.42, 0.04, -0.08]} rotation={[-Math.PI / 2, 0, 0]} />
    </group>
  )
}

Note that similar to --transform it also has to transform the model. In order to use and re-use the model import both Instances and Model. Put all your models into the Instances component (you can nest).

The following will show the model three times, but you will only have 2 drawcalls tops.

import { Instances, Model } from './Model'

<Instances>
  <Model position={[10,0,0]}>
  <Model position={[-10,0,0]}>
  <Model position={[-10,10,0]}>
</Instance>

API access

This package is split into two entrypoints, one for the CLI, one for the API. You can use parts of, or advanced features of the API that are not easily exposed via command line e.g. exposeProps in GenerateR3F.

The API is broken down with the intent of supporting external use cases that generate code for component systems other than react-three-fiber. With that said, it is the intent of this project to support react-three-fiber generation, and efforts for other frameworks may be determined to be out of scope.

API organization

  • Transform - gltfTransform is an opinionated wrapper using glTF-Transform api
  • Load - loadGLTF is a small wrapper using maintained loaders from node-three-gltf
  • Analyze - AnalyzedGLTF deduplicates, prunes, and provides convenient accessors to the GLTF
  • Generate - GenerateR3F uses ts-morph to generate the source file, allowing for external changes or subclassing to customize behavior.

Transform

await gltfTransform(modelFile, toTransformedModelFile, options)

Load

import { DRACOLoader } from 'node-three-gltf'

let dracoLoader = null
try {
  dracoLoader = new DRACOLoader() // use a single instance for one to many models
  const modelGLTF = await loadGLTF(modelFile, dracoLoader)
} finally {
  dracoLoader?.dispose()
}

This can be useful to test GLTF assets (see test/loadGLTF.test.ts):

it('should have a scene with a blue mesh', async () => {
  const scene = await loadGLTF(modelFile, dracoLoader)
  expect(() => scene.children.length).toEqual(1)
  expect(() => scene.children[0].type).toEqual('mesh')
  expect(() => scene.children[0].material.color).toEqual('blue')
})

Analyze

You can subclass to modify behaviors, or provide additional or different pruning strategies.

const a = new AnalyzedGLTF(modelGLTF, { options })

Generate

Generate a tsx or jsx file. Access ts-morph primitives directly on the GenerateR3F class to modify before formatting/stringifying with the to methods.

const g = new GenerateR3F(a, genOptions)

// modify
g.src.insertStatements(1, `const foo = 'bar'`)

// stringify with or without types
g.toJsx()
g.toTsx()

exposeProps - simple example

Instead of instrumenting code by hand, especially for large models, you can exposeProps that map the component props to arbitrary jsx children.

For example, if you want to be able to turn on/off shadows via shadows: true property, the following options:

  exposeProps: {
    shadows: {
      to: ['castShadow', 'receiveShadow'],
      structure: {
        type: 'boolean',
        hasQuestionToken: true,
      },
    },
  },

would generate code using ts-morph that will:

  • Add all mapped props to the ModelProps interface
  • Destructure variables in the function body with a ...rest
  • Change the identifer on the root <group {...rest} /> element
  • Set the argument in the function signature

This roughly equates to this output:

export interface FlightHelmetProps extends GroupProps {
  shadows?: boolean
}

export function FlightHelmet(props: FlightHelmetProps) {
  const { nodes, materials } = useGLTF(modelLoadPath, draco) as FlightHelmetGLTF
  const { shadows, ...rest } = props

  return (
    <group {...rest} dispose={null}>
      <mesh castShadow={shadows} receiveShadow={shadows} />
    </group>
  )
}

NOTE: if the Object3D property does not exist as part of calculated properties, it cannot be known at generation time that it is safe to add. If you want to force it to be added, use a matcher (see next section).

exposeProps - advanced example with matcher

A more powerful option is using the match specific nodes and expose those properties. Observe the following pseudo types (truncated for simplicity, see source for full type information):

interface GenerateOptions {
  /**
   * Expose component prop and propagate to matching Object3D props
   * e.g. shadows->[castShadow, receiveShadow]
   */
  exposeProps?: Record<string, MappedProp>
}

interface MappedProp {
  /**
   * Object3D prop(s)
   * e.g. castShadow | [castShadow, receiveShadow]
   * */
  to: string | string[]
  /**
   * Match a specific type of object.
   * If not provided, matches all that have the {to} prop
   * */
  matcher?: (o: Object3D, a: AnalyzedGLTF) => boolean
  /**
   * ts-morph prop structure (name is already supplied)
   * */
  structure: Omit<OptionalKind<PropertySignatureStructure>, 'name'>
}

This allows flexible exposure on arbitrary elements specified by the matcher. For example, if I wanted to toggle visibility on/off for some named elements:

for (const search of ['base', 'side', 'top']) {
  options.exposeProps![search] = {
    to: ['visible'],
    structure: {
      type: 'boolean',
      hasQuestionToken: true,
    },
    matcher: (o, a) =>
      // note isGroup doesn't work, because the model may be Object3D that is converted at generation time
      (isMesh(o) || getJsxElementName(o, a) == 'group') && o.name?.toLowerCase().includes(search),
  }
}

this would map:

export interface FooProps extends GroupProps {
  base?: boolean
  side?: boolean
  top?: boolean
}

to related elements. Because a matcher is specified, the property will be added to every element matched.

Requirements

About

🎮 Turns GLTFs into JSX components

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 90.8%
  • JavaScript 8.3%
  • Shell 0.9%