Need to set roughness texture without caring about metallic

Hi all

SO I get it , metallic , roughness ( and AO ) are all just single scalar values and it optimal to then use a single texture for all three…

except when you have predefined assets like roughness maps.

The optimal workflow should be a “nice to have” but definitely not “the only possible way”

has nobody ever crossed this need yet… Why cant we have a function to accept a roughness texture , then internally process its values ( obviously from R or G or B channels ) to the roughness of the material?

We have some variations on how to retrieve roughness, metallness and AO values of a texture (see useRoughnessFromMetallicTextureAlpha, useRoughnessFromMetallicTextureGreen, useMetallnessFromMetallicTextureBlue, useAmbientOcclusionFromMetallicTextureRed) but it would be too complicated to support all possible combinations (like having a different texture for each value, or one texture for roughness+metallness and another for AO, and so on).

If your assets are not designed this way, you should preprocess them to avoid having to rebuild them at runtime, although this could be a nice feature to have, at least for a quick demo / prototyping (but using GPU-compressed textures would not work, as you cannot recompress them on the client side). @Pryme8 is making a tool that let’s you create a texture from different source textures: Image Channel Swizzler

i dont see the need to keep the single texture after it is applied? , so internally you still maintain one single texture , with the texture channels dedicated to the various material channels as per usual.

then perhaps an extract function on an uploaded texture that has 3 booleans for what channels from the texture to extract. The texture upload should also dispose the texture after data is extracted. The materials channel texture setter function will decide what it does with the returned arrays of pixels from extract function.

So you still only have one internal texture as per usual and now you have facility to update the metalness or the roughness or the AO from any preexisting asset piplines that had dedicated textures for these values.

eg:

//extract(R,G,B)
let roughnessTexture = new Texture("path");
material.MRATexture = roughnessTexture.extract(false,true,false)
roughnessTexture.dispose();

// above im only going to update the roughness
// of the internal texture from the uploaded texture,

obviously you need to assume some default behaviours. For example if its the first time setting the texture , you need to then create the internal texture , perhaps with all pixels black? Then based on what you upload and choose to assign you just overwrite as needed. Matching the size should also have some consideration. Should it be set by the first source image size used? Possibly. If a different size is then used afterwards either it is rejected or filtered? I dont know. Anyway , apart from those technicalities , the other functionality seems like a doable idea?

Yes, but that would be something separated from the materials, like a new tool that would allow to generate a new texture based on some source texture(s) and a description on which channels to extract and where to put them in the new texture. A bit like what @Pryme8 is doing.

ok agreed , it is probably more a texture utility logic and doesnt belong directly in the material source.

the tool from @Pryme8 looks great thanks. I will probably be doing a pure code solution myself for my usage. I dont want to preprocess new images on disc.

thanks

You could always just extend your material and then swizzle your channels around in the shader code.

You do have a direct way to update the AO via a dedicated AO texture , by setting the ambientTexture property of the material. So you do have some overlap here and in the exact manner i desire :wink:

So you can put AO in a channel of the metalness texture … but you can also just directly assign it…

Why can roughness not have this same logic as well. ? Not to argue, just pointing out you actually do have this logic in place for one of the channels

so i want to just make this work without changing much of my app ,

so currently in the app I get texture paths and channel names in the data , textures get created and assigned to the channel.

eg, this updates diffuse :

<MATERIAL_UPDATE>
        <MATERIAL_KEY><![CDATA[faucet_T17453_textured]]></MATERIAL_KEY>
        <TEXTURE_KEY><![CDATA[faucet_T17453_stainless_diffuse]]></TEXTURE_KEY>
        <CHANNEL_KEY><![CDATA[albedoTexture]]></CHANNEL_KEY>
</MATERIAL_UPDATE>

so now im thinking to just add this for roughness , but pointing to a channel name on the material that doesnt exist , eg :

<MATERIAL_UPDATE>
        <MATERIAL_KEY><![CDATA[faucet_T17453_textured]]></MATERIAL_KEY>
        <TEXTURE_KEY><![CDATA[faucet_T17453_roughness_1]]></TEXTURE_KEY>
        <CHANNEL_KEY><![CDATA[roughnessTexture]]></CHANNEL_KEY>
</MATERIAL_UPDATE>

then write a setter onto the material prototype named “roughnessTexture”

when it receives the texture , i can then change the R channel to use the current metalness of the material ( per pixel values or the scalar as value ) and then set it to the metalness texture property of the material

Because there are too many cases to manage when we combine the number of channels of a texture (4) and the 3 properties roughness, metalness and AO => the existing variability is managed directly at the shader code level, we do not create temporary textures. So the code is already a bit complex because of that.

The current state comes largely from the support of the glTF format, I think. Before we had to support it, I think the roughness, metalness and AO values were all in a fixed place in a fixed texture, and because of glTF we had to adapt because that data was not in the same place that we had it. That’s why we added some options for where this data might be coming from.

I would extend the material you are using to add a define and just add the logic to switch out the part that samples the roughness for your material sample and creates the correct buffer samplers. That way you can just use the default stuff, but if you have a special case like you are talking about you can support that as well.

There should be a way to change the default material BJS uses as well so you would just make this extended material replace that and you should be good to go I would assume.

ok i need a little help because it almost works but something is missing , i wrote this :

Object.defineProperty(PBRBaseMaterial.prototype, "roughnessTexture", {
  configurable: true,
  set: function (newValue) {
    let texBuff;
    newValue.readPixels().then((val) => {
      texBuff = val;
      for (let i = 0; i < texBuff.length; i++) {
        if (i % 4 === 0) {
          texBuff[i] = 255;
        }
      }
      let tex = new Texture(null, null, null, null, null, null, null, texBuff);     
      this.metallicTexture = tex;
    });
  },
});

im just setting R to white directly , logged that array , i see the value updated , i then try create a texture using the array and set it to metallicTexture , it sets it but the texture object has no width or height or format
( i tried setting the format , but heaven alone knows what number to use , its not documented )
(also , i will cache this as well so I dont do it each time the same roughness is loaded )

also another question im already wondering

what makes the material know to read the AO from this texture or not? I mean you cant get rid of the channel being there , so are you forced to make it pure white as well so it has no effect?

Also , if the material already has a dedicated AO ( and in this case , mine do , GLB export from blender with AO included ) how does the material know which to use , I presume the dedicated texture just has preference over the channel for AO in metallicTexture?

I think you might just be making the texture incorrectly?

Also instead of doing i++ do i+=4 it will cut the number of loops by quite a bit and then just do

texBuff[i] = 255;
texBuff[i +1] = 0;
texBuff[i + 2] = 0;
texBuff[i + 3] = 255;

Pretty sure that is faster.

I am also not sure how creating a texture from just an array works since there would be no reference of width or height for it to know about.

Perhaps a dynamic texture might be better? Then you could create a dt with the correct width and height then slap that modified buffer onto it, fire an update to the dt wait for it to be ready then bind it.

Thanks , I adjusted the loop to be optimal as you have it, i tried dynamic texture :

  let dimention = Math.sqrt (texBuff.length/4);     
  let tex = new DynamicTexture(newValue.name, dimention);
  tex.updateURL(null, texBuff);
  this.metallicTexture = tex;

this creates a better texture object , but the width and height are incorrect. I know my texture is 512 , the dimention variable value is also 512 , so the pixel count is correct for 512. I tried setting it with an object with width and height properties as per the docs , both result in this 256 width and height?

also, the values in the channels are not being set correctly? ( if i log it the values look correct ) im not setting the G channel because this is the roughness value I want to use. if I use the inspector and view the R channel it is blank (checkerboard) , G is as in image below , B is something inbetween? so close … haha I hope I get this solved with this shortcut hack haha

weirdly the checkerboard is showing up on the model! ( i thought it just represents alpha in the inspector )

so i wonder, why is the width and height 256???

tex

ok if i dont set the buffer , the width and height are correct as created , so digging further I see an error object created on the dynamic texture that says there was a load error. this error is generated from trying to set the buffer. , I even removed all code updating the array that is returned from readPixels() and just passed that into the updateURL function and it still has the load error , i guess thats why its creating the 256 dimention texture with checkerboarding…

let texBuff;
newValue.readPixels().then((val) => {
  texBuff = val;
 /* for (let i = 0; i < texBuff.length; i += 4) {
    texBuff[i] = 255;
    texBuff[i + 2] = 255;
    texBuff[i + 3] = 255;
  }*/
  let dimention = Math.sqrt (texBuff.length/4);     
  let tex = new DynamicTexture(newValue.name, dimention,scene,true); 
  tex.updateURL(null, texBuff);
  this.metallicTexture = tex;

readPixels is returning a ArrayBufferView according to the docs and this is a valid array to set as the buffer … so why is there a load error i wonder…?

Try creating a RawTexture. It’s the preferred way to create a texture from raw data.

2 Likes

PERFECT!

Object.defineProperty(PBRBaseMaterial.prototype, "roughnessTexture", {
    configurable: true,
    set: function (newValue) {
      if (this[newValue.name]) {
        this.metallicTexture = this[newValue.name];
        return;
      }
      let texBuff;
      newValue.readPixels().then((val) => {
        texBuff = val;
        for (let i = 0; i < texBuff.length; i += 4) {
          texBuff[i] = 255;
          texBuff[i + 2] = 255;
          texBuff[i + 3] = 255;
        }
        let dimention = Math.sqrt(texBuff.length / 4);
        this[newValue.name] = new RawTexture(
          texBuff,
          dimention,
          dimention,
          5,
          scene,
          true
        );
        this.useRoughnessFromMetallicTextureAlpha = false;
        this.useRoughnessFromMetallicTextureGreen = true;
        this.metallicTexture = this[newValue.name];
      });
    },
  });

i had to add :

this.useRoughnessFromMetallicTextureAlpha = false;
this.useRoughnessFromMetallicTextureGreen = true;

ps… i had to test each value for the format number because the docs dont link to anything for this , 0 was alpha , 1 was luminance , 2 was alpha/luminance , 3 was unknown , 4 was RGB and finally 5 was RGBA.

I know the caching is a real hack , well everything is - but it works ! I know about technical dept and all , but for now this was a huge time saver because i didnt have to change anything else in the system ( and if I ever have to use sucha patch again … im sure i will )

There are some constants you can use: BABYLON.Constants.TEXTUREFORMAT_RGBA for eg. They all start with TEXTUREFORMAT_ so that it’s easy to find them.

1 Like