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

Adding metallic feature in p5.js for both IBL and non-IBL codes. #6618

Merged
merged 31 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/webgl/light.js
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,7 @@ p5.prototype.noLights = function(...args) {
this._renderer.linearAttenuation = 0;
this._renderer.quadraticAttenuation = 0;
this._renderer._useShininess = 1;
this._renderer._useMetalness = 0;

return this;
};
Expand Down
70 changes: 70 additions & 0 deletions src/webgl/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,76 @@ p5.prototype.shininess = function (shine) {
return this;
};

/**
* Sets the metalness property of a material used in 3D rendering.
*
* The metalness property controls the degree to which the material
* appears metallic. A higher metalness value makes the material more
* metallic, while a lower value makes it appear less metallic.
*
* The default and minimum value is 0, indicating a non-metallic appearance,
* while a maximum value of 100 represents a fully metallic appearance.
*
* @method metalness
* @param {Number} metallic - The degree of metalness (ranging from 0 to 100).
* @example
* <div class="notest">
* <code>
* let img;
* let slider;
* function preload() {
* img = loadImage('assets/outdoor_spheremap.jpg');
* }
* function setup() {
* createCanvas(100, 100, WEBGL);
* slider = createSlider(0, 400, 100, 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe for this slider (and in the next example if we add a slider) we can add a label next to it to say what parameter it's controlling?

* slider.position(0, height);
* }
* function draw() {
* background(220);
* imageMode(CENTER);
* push();
* translate(0, 0, -200);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now have a clearDepth method, maybe this could be simplified to just image(img, 0, 0, width, height) followed by clearDepth() without translating backwards and scaling?

* scale(2);
* image(img, 0, 0, width, height);
* pop();
* imageLight(img);
* specularMaterial('gray');
* shininess(slider.value());
* metalness(100);
* noStroke();
* scale(2);
* sphere(15);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of scaling before this, could we just do sphere(30)?

* }
* </code>
* </div>
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100, WEBGL);
* }
* function draw() {
* noStroke();
* background('black');
* fill(255, 215, 0);
* pointLight(255, 255, 255, 200, 150, 8000);
* pointLight(255, 255, 255, 5000, 5000, 75);
* specularMaterial('gray');
* shininess(2);
* metalness(100);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great for one of the examples to have a slider that controls metalness so people can see the effect it has. Maybe this one would be good for that? A quick update to this that I was trying out:

let slider
function setup() {
  createCanvas(100, 100, WEBGL);
  slider = createSlider(0, 200, 100);
}
function draw() {
  noStroke();
  background(100);
  fill(255, 215, 0);
  pointLight(255, 255, 255, 5000, 5000, 75);
  specularMaterial('gray');
  ambientLight(100);
  shininess(2);
  metalness(slider.value());
  sphere(45);
}

The things I was hoping to improve upon were:

  • added a slider to control metalness
  • a different background color so you can see the outline of the shape better
  • lights that show the form of the shape a bit more clearly (it's definitely still not perfect, any ideas on how to improve this? maybe making the light orbit the sphere over time? maybe using a torus instead of a sphere?)
  • less ambient light so the non-metal version is less blown out and you can see the highlights better

* sphere(45);
* }
* </code>
* </div>
*/

p5.prototype.metalness = function (metallic) {
this._assert3d('metalness');
this._renderer._useMetalness = metallic;
return this;
};

/**
* @private blends colors according to color components.
* If alpha value is less than 1, or non-standard blendMode
Expand Down
14 changes: 14 additions & 0 deletions src/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
this._useEmissiveMaterial = false;
this._useNormalMaterial = false;
this._useShininess = 1;
this._useMetalness = 0;

this._useLineColor = false;
this._useVertexColor = false;
Expand Down Expand Up @@ -1613,6 +1614,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
properties._useSpecularMaterial = this._useSpecularMaterial;
properties._useEmissiveMaterial = this._useEmissiveMaterial;
properties._useShininess = this._useShininess;
properties._useMetalness = this._useMetalness;

properties.constantAttenuation = this.constantAttenuation;
properties.linearAttenuation = this.linearAttenuation;
Expand Down Expand Up @@ -2039,6 +2041,17 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
_setFillUniforms(fillShader) {
fillShader.bindShader();

if (this._useMetalness > 0 && !this.curFillColor.every((value, index) =>
value === 1)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind explaining a bit what the scenario is that we're checking for here with the .every(...)? Is it necessary, since we don't do it for ambient colors?

const metalnessFactor = Math.min(this._useMetalness / 100, 0.6);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you have some visual examples that lead you to arrive at 0.6 as the max metalness and 0.4 as the min non-metalness instead of letting them both go from 0 to 1?

const nonMetalnessFactor = Math.max(1 - metalnessFactor, 0.4);

this.curSpecularColor = this.curSpecularColor.map(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By modifying its current value, I think that means the color will be slightly different for every successive shape we render. Instead maybe we can do something like let specularColor = [...this.curSpecularColor] outside of the if statement and modify that, and pass that in to the uniform on line 2066 below? That way we aren't storing the new color, so it gets recalculated fresh for the next object.

(specularColor, index) =>
this.curFillColor[index] * metalnessFactor +
specularColor * nonMetalnessFactor
);
}
// TODO: optimize
fillShader.setUniform('uUseVertexColor', this._useVertexColor);
fillShader.setUniform('uMaterialColor', this.curFillColor);
Expand All @@ -2055,6 +2068,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
fillShader.setUniform('uSpecular', this._useSpecularMaterial);
fillShader.setUniform('uEmissive', this._useEmissiveMaterial);
fillShader.setUniform('uShininess', this._useShininess);
fillShader.setUniform('metallic', this._useMetalness);

this._setImageLightUniforms(fillShader);

Expand Down
22 changes: 15 additions & 7 deletions src/webgl/shaders/lighting.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ uniform vec3 uSpotLightDirection[5];

uniform bool uSpecular;
uniform float uShininess;
uniform float metallic;

uniform float uConstantAttenuation;
uniform float uLinearAttenuation;
Expand Down Expand Up @@ -73,9 +74,14 @@ LightResult _light(vec3 viewDirection, vec3 normal, vec3 lightVector) {

//compute our diffuse & specular terms
LightResult lr;
if (uSpecular)
lr.specular = _phongSpecular(lightDir, viewDirection, normal, uShininess);
lr.diffuse = _lambertDiffuse(lightDir, normal);

float invertValue = 1.0 - (metallic / 100.0);
float specularIntensity = mix(0.4, 1.0, invertValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is equivalent to this:

Suggested change
float specularIntensity = mix(0.4, 1.0, invertValue);
float specularIntensity = mix(1.0, 0.4, metallic / 100.0);

Might be a bit easier to reason about without adding the extra in-between variable with the flipped value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it looks like these changes have a different indentation level compared to the surroinding code, we should probably make it match

float diffuseIntensity = mix(0.1, 1.0, invertValue);

if (uSpecular)
lr.specular = (_phongSpecular(lightDir, viewDirection, normal, uShininess)) * specularIntensity;
lr.diffuse = _lambertDiffuse(lightDir, normal) * diffuseIntensity;
return lr;
}

Expand Down Expand Up @@ -114,7 +120,8 @@ vec3 calculateImageDiffuse( vec3 vNormal, vec3 vViewPosition ){
vec4 texture = TEXTURE( environmentMapDiffused, newTexCoor );
// this is to make the darker sections more dark
// png and jpg usually flatten the brightness so it is to reverse that
return smoothstep(vec3(0.0), vec3(0.8), texture.xyz);
float invertedMetallic = 1.0 - metallic / 100.0;
return mix(vec3(0.0), smoothstep(vec3(0.0), vec3(1.0), texture.xyz), invertedMetallic);
}

vec3 calculateImageSpecular( vec3 vNormal, vec3 vViewPosition ){
Expand All @@ -130,7 +137,9 @@ vec3 calculateImageSpecular( vec3 vNormal, vec3 vViewPosition ){
#endif
// this is to make the darker sections more dark
// png and jpg usually flatten the brightness so it is to reverse that
return pow(outColor.xyz, vec3(10.0));
float invertedMetallic = 1.0 - (metallic / 100.0);
float mappedValue = mix(1.2, 10.0, invertedMetallic);
return pow(outColor.xyz, vec3(mappedValue));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that as I bright the metalness slider up, in the middle, the shadows dip darker before they finally brighten:

image image image

I wonder if this is because we change the specular/diffuse balance linearly, but the power exponentially (since we linearly interpolate the exponent.) Maybe we'll avoid this dip if, instead of linearly interpolating the exponent, we linearly mix between to fixed exponents, like this?

return mix(
  pow(outColor.xyz, vec3(1.2)),
  pow(outColor.xyz, vec3(10)),
  invertedMetallic
);

}

void totalLight(
Expand All @@ -141,7 +150,6 @@ void totalLight(
) {

totalSpecular = vec3(0.0);

if (!uUseLighting) {
totalDiffuse = vec3(1.0);
return;
Expand All @@ -164,7 +172,7 @@ void totalLight(
if (j < uPointLightCount) {
vec3 lightPosition = (uViewMatrix * vec4(uPointLightLocation[j], 1.0)).xyz;
vec3 lightVector = modelPosition - lightPosition;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some extra spaces got added here

//calculate attenuation
float lightDistance = length(lightVector);
float lightFalloff = 1.0 / (uConstantAttenuation + lightDistance * uLinearAttenuation + (lightDistance * lightDistance) * uQuadraticAttenuation);
Expand Down
2 changes: 1 addition & 1 deletion test/unit/core/rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ suite('Rendering', function() {
'model',
'shader',
'normalMaterial', 'texture', 'ambientMaterial', 'emissiveMaterial', 'specularMaterial',
'shininess', 'lightFalloff',
'shininess', 'lightFalloff', 'metalness',
'plane', 'box', 'sphere', 'cylinder', 'cone', 'ellipsoid', 'torus'
];

Expand Down
Loading