pmndrs / drei

🥉 useful helpers for react-three-fiber
https://drei.pmnd.rs/
MIT License
7.84k stars 641 forks source link

Using MeshRefractionMaterial in non-React code #1181

Closed gydence closed 1 year ago

gydence commented 1 year ago

Hi! I'm very new to React so apologies if this question doesn't make sense.

I'm working on a simple React app using A-Frame and I'm hoping to incorporate elements from drei (e.g. MeshRefractionMaterial). I just have a React component that has a top level <a-scene> and then some entities.

Is there a "proper" way to mix drei components with say, a custom A-Frame JS component? I can't just instantiate it in JS:

let material = new MeshRefractionMaterial({});

I understandably get:

Failure loading node:   Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

So I tried:

<a-scene>
  <Canvas>
    <MeshRefractionMaterial id="refractionMaterial" bounces={2} aberrationStrength={0.01} envMap={texture} toneMapped={false} />
  </Canvas>
</a-scene>

and then grabbing it in JS with:

document.querySelector("#refractionMaterial");

But that gives me:

Cannot assign to read only property 'id' of object '#<material>'

And even if the id worked, I'm not sure this is the correct way of doing this.

Any help is much appreciated, thank you!

EDIT:

Aha! I see that there are actually two MeshRefractionMaterials in the package, one from @react-three/drei, which is the React component, and one from @react-three/drei/materials/MeshRefractionMaterial which is the JS material I want, just without the setup. So I can do:

import { MeshRefractionMaterial } from "@react-three/drei/materials/MeshRefractionMaterial";
import { MeshBVH, SAH } from "three-mesh-bvh";
...
    // set the scene environment, which gets mapped to the material's envMap
    const pmremGenerator = new THREE.PMREMGenerator(renderer);
    scene.environment = pmremGenerator.fromScene(environment).texture;
...
    // initialize the material
    this.diamondMaterial = new MeshRefractionMaterial();
    this.diamondMaterial.uniforms.bounces.value = 2;
    this.diamondMaterial.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);

    {
      const isCubeMap = scene.environment.isCubeTexture === true;
      this.diamondMaterial.defines = {
        ENVMAP_TYPE_CUBEM: isCubeMap,
        CHROMATIC_ABERRATIONS: false,
        FAST_CHROMA: true
      };
    }
...
    // later, when a mesh loads, apply the material:
    self.el.object3D.traverse(function (node) {
      node.material = self.diamondMaterial.clone();
      node.material.uniforms.bvh.value.updateFrom(new MeshBVH(node.geometry.toNonIndexed(), {
        lazyGeneration: false,
        strategy: SAH
      }));
  });

This gets me much closer, but now I see the mesh rendering all black and I get:

three.js:14285 THREE.WebGLProgram: Program Info Log: C:\fakepath(271,1-6): warning X4000: use of potentially uninitialized variable (dyn_index_vec3_int)

debug.js:39 THREE.WebGLState: TypeError: Failed to execute 'texSubImage2D' on 'WebGL2RenderingContext': Overload resolution failed.
    at e.executeFunction (<anonymous>:3:467593)
    at e.executeOriginFunction (<anonymous>:3:467224)
    at WebGL2RenderingContext.texSubImage2D (<anonymous>:3:499508)
    at Object.texSubImage2D (three.js:16431:1)
    at uploadTexture (three.js:17275:1)
    at WebGLTextures.setTexture2D (three.js:16905:1)
    at SingleUniform.setValueT1 [as setValue] (three.js:13414:1)
    at WebGLUniforms.upload (three.js:13905:1)
    at setProgram (three.js:20844:1)
    at WebGLRenderer.renderBufferDirect (three.js:20165:1)

EDIT 2:

It seems that the problem was the PMREM texture. If I instead load a texture using TextureLoader (and set some of the same props that the React MeshRefractionMaterial normally does for you):

    const texture = new THREE.TextureLoader().load(process.env.PUBLIC_URL + "/assets/card.png",
      function (texture) {
        self.diamondMaterial.uniforms.envMap.value = texture;
        self.diamondMaterial.uniforms.envMap.value.needsUpdate = true;
        const isCubeMap = texture.isCubeTexture === true;
        const w = isCubeMap ? texture.image[0].width : texture.image.width;
        const cubeSize = 0.25 * w;
        const _lodMax = Math.floor(Math.log2(cubeSize));
        const _cubeSize = Math.pow(2, _lodMax);
        const width = 3 * Math.max(_cubeSize, 16 * 7);
        const height = 4 * _cubeSize;
        self.diamondMaterial.defines = {
          ENVMAP_TYPE_CUBEM: isCubeMap,
          CUBEUV_MAX_MIP: `${_lodMax}.0`,
          CUBEUV_TEXEL_WIDTH: 1 / width,
          CUBEUV_TEXEL_HEIGHT: 1 / height,
          CHROMATIC_ABERRATIONS: false,
          FAST_CHROMA: false
        };
        self.diamondMaterial.needsUpdate = true;
      });

The errors go away, but the diamond renders mostly black with a sliver of my envMap colors around the edge: diamond

gydence commented 1 year ago

That last issue ended up being an issue in the drei shader. 'vNormal` isn't normalized, so this fixes the issue:

    vNormal = normalize((viewMatrixInv * vec4(normalMatrix * transformedNormal.xyz, 0.0)).xyz);
shrpne commented 2 days ago

@gydence Sorry, this issue is outdated, but are you able to share the final code?