Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for generating slots from blender custom properties #265

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

marwie
Copy link

@marwie marwie commented Jun 21, 2024

This PR allows to use custom properties defined in Blender to be used as custom react slots.

  • It makes it easier to re-use generated components and in defeats the need of modifying generated code altogether in cases where e.g. custom components need to be injected somewhere in the hierarchy.
  • It also allows to attach into parent components to change properties from the outside

Example

https://codesandbox.io/p/sandbox/xenodochial-dawn-77st43

Usage:

  1. create a custom string property in Blender and name it slot and enter a value e.g. "mySlot"
  2. export to glTF or GLB (make sure to enable Include/Custom Properties in the Exporter)
  3. use gltfjsx myModel.glb as usual
  4. see the gltfjsx output has generated { props.mySlot } which can now be access from outside

Example

import { Model as Tower } from './res'
...
{/** Inject objects */}

<Tower
  roof={
    <mesh position={[0, 0, 12]}>
      <boxGeometry args={[5, 1, 5]} />
      <meshStandardMaterial color={'blue'} />
    </mesh>
  }></Tower>

<Tower
  position={[10, 0, 0]}
  roof={
    <mesh position={[0, 0, 13]}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={'red'} />
    </mesh>
  }></Tower>
  
{/** Modify properties */}

<Tower position={[20, 0, 0]} roof={<meshStandardMaterial attach="material" color={'yellow'} />}></Tower>

Codegen

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.2.18 C:\Users\marce\Downloads\New folder (38)\public\untitled.glb --output C:\Users\marce\Downloads\New folder (38)\src\res.jsx --transform 
Files: C:\Users\marce\Downloads\New folder (38)\public\untitled.glb [23.15MB] > C:\Users\marce\Downloads\New folder (38)\src\untitled-transformed.glb [2.17MB] (91%)
*/

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

export function Model(props) {
  const { nodes, materials } = useGLTF('/untitled-transformed.glb')
  return (
    <group {...props} dispose={null}>
      <mesh geometry={nodes.Tower_Brick_MT_0.geometry} material={materials.Brick_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Details_MT_0.geometry} material={materials.Details_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Glass_MT_0.geometry} material={materials.Glass_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Plaster_MT_0.geometry} material={materials.Plaster_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Roof_MT_0.geometry} material={materials.Roof_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        {props.roof}
      </mesh>
    </group>
  )
}

useGLTF.preload('/untitled-transformed.glb')

Screenshots

20240621-153139_index.js_-_nodebox_-_CodeSandbox_-_Google_Chrome-logo.mp4

image

image

Further Ideas (not implemented)

Props could possibly also / instead be functions that take a ref argument - perhaps with a different property name in blender like callback: "myCallback". I'm not sure about drawbacks of potentially creating a lot of refs vs just injecting properties and/or if this is already possible with the code in this PR through some other hook.

<Tower position={[10, 0, 0]}
  roof={(ref) => {
    useFrame(()=>{
      ref.current.rotation.z += 0.05;
    })
  }}>
</Tower>

E.g. codegen would then create refs for all objects that need them and invoke the callback methods

export function Model(props) {
  const { nodes, materials } = useGLTF('/untitled-transformed.glb')
  const roof1 = useRef();
  return (
    <group {...props} dispose={null}>
      <mesh geometry={nodes.Tower_Brick_MT_0.geometry} material={materials.Brick_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Details_MT_0.geometry} material={materials.Details_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Glass_MT_0.geometry} material={materials.Glass_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Plaster_MT_0.geometry} material={materials.Plaster_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh ref={roof1} geometry={nodes.Tower_Roof_MT_0.geometry} material={materials.Roof_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        {props.roof(roof1)}
      </mesh>
    </group>
  )
}

This PR is funded by needle

@marwie marwie changed the title Add support for generating slots from blender properties Add support for generating slots from blender custom properties Jun 21, 2024
@Ctrlmonster
Copy link

Ctrlmonster commented Jun 21, 2024

Very valuable addition in my view. I usually have to make a lot of manual additions to my gltfjsx output (i.e. adding custom children into the tree) and whenever the model changes in any significant way those changes to the jsx get lost.

// generated gltfjsx 
function Tower() {
  return (
    <mesh material={myMaterial} geometry={towerGeometry} />
      {props.lightsource}
    </mesh>
  )
}


// use gltfjsx component throughout the app with different elements injected into the generated code

<>
  {/*actual three light*/}
  <Tower lightsource={<pointLight intensity={0.5} />} />
  
  {/*or with a sprite animation*/}
  <Tower lightsource={<SpriteAnimator src={fireSprite} loop />}
  
  {/*or with a custom vfx effect component*/}
  <Tower lightsource={<VfxSpawner preset={"fire"} />}

</>

Here is a real world example where the castle environment is quite complex and ran through gltfjsx (couple hundred LOC). I had to manually search for these pillars in the generated code to insert my animated sprites. With this change I could already mark these pillars inside blender and just pass the sprite component from the outside, not having to worry about changes that will be made to the environment in the future.
fire

@drcmda
Copy link
Member

drcmda commented Jun 21, 2024

@marwie i think it should be

export function Model({ foo, bar, ...props }) {
  const { nodes, materials } = useGLTF('/untitled-transformed.glb')
  return (
    <group {...props} dispose={null}>
      <mesh geometry={nodes.Tower_Brick_MT_0.geometry} material={materials.Brick_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Details_MT_0.geometry} material={materials.Details_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Glass_MT_0.geometry} material={materials.Glass_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]} />
      <mesh geometry={nodes.Tower_Plaster_MT_0.geometry} material={materials.Plaster_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        {foo}
      </mesh>
      <mesh geometry={nodes.Tower_Roof_MT_0.geometry} material={materials.Roof_MT} position={[0, 7.561, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        {bar}
      </mesh>
    </group>
  )
}

otherwise it would spread props all over the root mesh (and could also introduce some real problems, like naming a slot "position". it would require though that slots for all nodes are known beforehand, just some javascript mapping and reducing.

@marwie
Copy link
Author

marwie commented Jun 21, 2024

Ah that's a good point. I missed that. I think currently all objects are collected at the start of codegen anyways so collecting that info could be added there.

What do you think about the callbacks idea (or slots becoming functions) what I mentioned at the end of the post?
(to allow something like (ref) => useFrame(...))

@drcmda
Copy link
Member

drcmda commented Jun 21, 2024

Ah that's a good point. I missed that. I think currently all objects are collected at the start of codegen anyways so collecting that info could be added there.

What do you think about the callbacks idea (or slots becoming functions) what I mentioned at the end of the post? (to allow something like (ref) => useFrame(...))

wouldn't be necessary imo. defining functions inline is considered bad because this isn't a true component, it would un-mount/re-mount every render. if it contained useEffect(() => ..., []) it would fire every render as well.

as for accessing parents, you can do this

<Tower roof={<Foo />} />

function Foo(props) {
  const ref = useRef()
  useLayoutEffect(() => {
    console.log(ref.current.parent)
  }, [])
  useFrame((state, delta) => {
    console.log(ref.current.parent)
  })
  return <group ref={ref} />
}

@marwie
Copy link
Author

marwie commented Jun 21, 2024

Any thoughts on the naming of prop vs slot for the custom data field?

The default blender name is prop which also seems fitting and it would save a few clicks in Blender of renaming "prop" to "slot" and users would just have to change the type to a String to become useable.

image

@marwie
Copy link
Author

marwie commented Jun 21, 2024

Example output:

export function Model({ cat, screen, ...props }) {
  const { nodes, materials } = useGLTF('/untitled.glb')
  return (
    <group {...props} dispose={null}>
      <group rotation={[-Math.PI / 2, 0, 0]} scale={0.002}>
        <group rotation={[Math.PI / 2, 0, 0]}>
          <group>
            <mesh geometry={nodes.defaultMaterial001.geometry} material={materials.wire_134006006} />
            {cat}
          </group>
          <group>
            <mesh geometry={nodes.defaultMaterial.geometry} material={materials.Screen} />
            {screen}
          </group>
        </group>
      </group>
    </group>
  )
}

Objects that have a slot property would also not be pruned with this change.

@Ctrlmonster
Copy link

Wondering, could it make sense to have a differentiation between something like childSlots and propSlots, for the cases where you'd want to set an actual prop (i.e. position, visibility, etc.) from the outside, vs. injecting elements into the tree?

Other than that, very much looking forward to this, being able to decouple the generated code and lift dependencies to the parent component, not having to re-edit the jsx every time you update your meshes/scene will help a lot with more demanding usecases where you are iterating a lot (like gamedev).

@drcmda
Copy link
Member

drcmda commented Jun 24, 2024

Any thoughts on the naming of prop vs slot for the custom data field?

The default blender name is prop which also seems fitting and it would save a few clicks in Blender of renaming "prop" to "slot" and users would just have to change the type to a String to become useable.

image

no issue with that, let's make it the closest to blender defaults

@marwie
Copy link
Author

marwie commented Jun 26, 2024

Let me know if there's anything else you'd like to change

@krispya
Copy link
Member

krispya commented Jun 28, 2024

I was wondering how this works with multiple slots. I was testing the process on my end and you can only add one slot custom prop. Trying to add another merges into the first. Would it be supported with some kind of symbol, like a , ?

@marwie
Copy link
Author

marwie commented Jun 28, 2024

@krispya You mean adding multiple slots to the same object in blender? When would you want to do that for example?

@krispya
Copy link
Member

krispya commented Jun 28, 2024

I guess you are right, I was considering something more like a registry. If there is a name collision what happens?

@marwie
Copy link
Author

marwie commented Jun 28, 2024

Multiple objects can have the same slot name (e.g. you can add the same custom property slot name in blender to multiple objects).

The slotted objects will then appear in multiple places.

export function Model({ my_slot, ...props }) {
  const { nodes, materials } = useGLTF('/untitled.glb')
  return (
    <group {...props} dispose={null}>
      <group rotation={[-Math.PI / 2, 0, 0]} scale={0.002}>
        <group rotation={[Math.PI / 2, 0, 0]}>
          <group>
            <mesh geometry={nodes.defaultMaterial001.geometry} material={materials.wire_134006006} />
            {my_slot}
          </group>
          <group>
            <mesh geometry={nodes.defaultMaterial.geometry} material={materials.Screen} />
            {my_slot}
          </group>
        </group>
      </group>
    </group>
  )
}

Is that what you meant?

@Ctrlmonster
Copy link

What about multiple different props, i.e. my_slot1, my_slot2?

@marwie
Copy link
Author

marwie commented Jun 28, 2024

Like here? #265 (comment) Or do you mean something else?

@krispya
Copy link
Member

krispya commented Jun 28, 2024

Multiple objects can have the same slot name (e.g. you can add the same custom property slot name in blender to multiple objects).

The slotted objects will then appear in multiple places.

export function Model({ my_slot, ...props }) {
  const { nodes, materials } = useGLTF('/untitled.glb')
  return (
    <group {...props} dispose={null}>
      <group rotation={[-Math.PI / 2, 0, 0]} scale={0.002}>
        <group rotation={[Math.PI / 2, 0, 0]}>
          <group>
            <mesh geometry={nodes.defaultMaterial001.geometry} material={materials.wire_134006006} />
            {my_slot}
          </group>
          <group>
            <mesh geometry={nodes.defaultMaterial.geometry} material={materials.Screen} />
            {my_slot}
          </group>
        </group>
      </group>
    </group>
  )
}

Is that what you meant?

Looks good to me.

@Ctrlmonster
Copy link

Like here? #265 (comment) Or do you mean something else?

Ah yeah that's what I meant!

@marwie
Copy link
Author

marwie commented Jun 29, 2024

@drcmda just squashed and rebased on the latest release and updated the example here: https://codesandbox.io/p/sandbox/xenodochial-dawn-77st43

@marwie
Copy link
Author

marwie commented Jul 10, 2024

@drcmda let me know if you'd like to have anything else changed or checked

@drcmda
Copy link
Member

drcmda commented Jul 13, 2024

@marwie could you solve the conflicts?

@marwie marwie force-pushed the feature/custom-props branch 2 times, most recently from f3fc5a6 to 6d727db Compare July 13, 2024 21:12
@marwie
Copy link
Author

marwie commented Jul 13, 2024

@drcmda done

@marwie
Copy link
Author

marwie commented Aug 2, 2024

@drcmda hi do you have any further concerns regarding the feature? :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants