Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
34 changes: 33 additions & 1 deletion src/core/p5.Renderer3D.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export class Renderer3D extends Renderer {
this.states.drawMode = constants.FILL;

this.states._tex = null;
this.states._specularTex = null;
this.states._ambientTex = null;
this.states._shininessTex = null;
this.states.textureMode = constants.IMAGE;
this.states.textureWrapX = constants.CLAMP;
this.states.textureWrapY = constants.CLAMP;
Expand Down Expand Up @@ -611,7 +614,10 @@ export class Renderer3D extends Renderer {
state.texture != null ||
state.ambientColor != null ||
state.specularColor != null ||
state.shininess != null);
state.shininess != null ||
state.specularTexture != null ||
state.ambientTexture != null ||
state.shininessTexture != null);
if (hasMaterial) {
this.push();
this._applyPartState(state);
Expand Down Expand Up @@ -676,13 +682,26 @@ export class Renderer3D extends Renderer {
this.states.setValue('curAmbientColor', partState.ambientColor);
this.states.setValue('_hasSetAmbient', true);
}
if (partState.ambientTexture) {
// an ambient map modulates the ambient term, so make sure it is on
this.states.setValue('_ambientTex', partState.ambientTexture);
this.states.setValue('_hasSetAmbient', true);
}
if (partState.specularColor) {
this.states.setValue('curSpecularColor', partState.specularColor);
this.states.setValue('_useSpecularMaterial', true);
}
if (partState.specularTexture) {
// a specular map modulates the specular term, so make sure that term is on
this.states.setValue('_specularTex', partState.specularTexture);
this.states.setValue('_useSpecularMaterial', true);
}
if (partState.shininess != null) {
this.states.setValue('_useShininess', partState.shininess);
}
if (partState.shininessTexture) {
this.states.setValue('_shininessTex', partState.shininessTexture);
}
}

_drawStrokes(geometry, { count } = {}) {
Expand Down Expand Up @@ -1567,6 +1586,19 @@ export class Renderer3D extends Renderer {
fillShader.setUniform('uSampler', this.states._tex || empty);
}
this._settingFillUniforms = false;
// specular map (map_Ks): always bind so the sampler is valid; the bool gates
// whether the shader actually uses it, so untextured draws are unaffected.
fillShader.setUniform('uHasSpecularTex', !!this.states._specularTex);
fillShader.setUniform('uSpecularSampler', this.states._specularTex || empty);
// ambient map (map_Ka): same always-bind + bool-gate pattern
fillShader.setUniform('uHasAmbientTex', !!this.states._ambientTex);
fillShader.setUniform('uAmbientSampler', this.states._ambientTex || empty);
// shininess map (map_Ns): scales the base shininess by the map's red channel
fillShader.setUniform('uHasShininessTex', !!this.states._shininessTex);
fillShader.setUniform(
'uShininessSampler',
this.states._shininessTex || empty
);
fillShader.setUniform(
'uTint',
this.states.tint?._getRGBA([255, 255, 255, 255]) ?? [255, 255, 255, 255]
Expand Down
61 changes: 45 additions & 16 deletions src/webgl/loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ function parseMtlData(data) {
} else if (tokens[0] === 'map_Ks') {
//specular texture
materials[currentMaterial].specularTexturePath = tokens[1];
} else if (tokens[0] === 'map_Ns') {
//shininess texture
materials[currentMaterial].shininessTexturePath = tokens[1];
} else if (tokens[0] === 'map_Bump' || tokens[0] === 'bump') {
//bump map. -bm etc can precede the path so take the last token. parsed
//but not used until the renderer handles it.
Expand All @@ -98,13 +101,37 @@ function mtlToPartState(material) {
if (material.specularColor) state.specularColor = material.specularColor;
if (material.shininess !== undefined) state.shininess = material.shininess;
if (material.texture) state.texture = material.texture;
if (material.specularTexture) {
state.specularTexture = material.specularTexture;
// a specular map modulates a base specular colour; default to white so the
// map shows even when the mtl has a map_Ks but no explicit Ks colour.
if (!state.specularColor) state.specularColor = [1, 1, 1];
}
if (material.ambientTexture) {
state.ambientTexture = material.ambientTexture;
// same idea as the specular map: default the base ambient colour to white
if (!state.ambientColor) state.ambientColor = [1, 1, 1];
}
if (material.shininessTexture) {
state.shininessTexture = material.shininessTexture;
// the map scales the base shininess; default the base to 1 when no Ns
if (state.shininess == null) state.shininess = 1;
}
return state;
}

// load each material's diffuse texture (map_Kd) and hang it on the material so
// it lands on the part state. paths resolve relative to the model file, a
// texture that fails just gets skipped. no-op if there's no loadImage. only
// map_Kd for now since that's all the renderer can use.
// each texture map the renderer can use: the parsed path field on the material,
// and the image field we hang the loaded p5.Image on for mtlToPartState to read.
const MATERIAL_TEXTURE_MAPS = [
['texturePath', 'texture'], // map_Kd (diffuse)
['specularTexturePath', 'specularTexture'], // map_Ks (specular)
['ambientTexturePath', 'ambientTexture'], // map_Ka (ambient)
['shininessTexturePath', 'shininessTexture'] // map_Ns (shininess)
];

// load each material's texture maps and hang them on the material so they land
// on the part state. paths resolve relative to the model file, a texture that
// fails just gets skipped. no-op if there's no loadImage.
async function loadMaterialTextures(materials, modelPath, instance) {
if (!instance || typeof instance.loadImage !== 'function') return;

Expand All @@ -115,18 +142,20 @@ async function loadMaterialTextures(materials, modelPath, instance) {
const jobs = [];
for (const name in materials) {
const material = materials[name];
if (!material.texturePath) continue;
const url = resolve(material.texturePath);
jobs.push(
instance
.loadImage(url)
.then(img => {
material.texture = img;
})
.catch(() => {
console.warn(`Texture not found, skipping: ${url}`);
})
);
for (const [pathField, imageField] of MATERIAL_TEXTURE_MAPS) {
if (!material[pathField]) continue;
const url = resolve(material[pathField]);
jobs.push(
instance
.loadImage(url)
.then(img => {
material[imageField] = img;
})
.catch(() => {
console.warn(`Texture not found, skipping: ${url}`);
})
);
}
}

await Promise.all(jobs);
Expand Down
5 changes: 4 additions & 1 deletion src/webgl/p5.GeometryPart.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ function createPartState() {
ambientColor: null, // Ka -> [r, g, b] | null, each 0..1
specularColor: null, // Ks -> [r, g, b] | null, each 0..1
shininess: null, // Ns -> number | null
texture: null // map_Kd -> p5.Image | null
texture: null, // map_Kd -> p5.Image | null
specularTexture: null, // map_Ks -> p5.Image | null
ambientTexture: null, // map_Ka -> p5.Image | null
shininessTexture: null // map_Ns -> p5.Image | null
};
}

Expand Down
18 changes: 15 additions & 3 deletions src/webgl/shaders/phong.frag
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ uniform vec4 uEmissiveMatColor;
uniform vec4 uTint;
uniform sampler2D uSampler;
uniform bool isTexture;
uniform sampler2D uSpecularSampler;
uniform bool uHasSpecularTex;
uniform sampler2D uAmbientSampler;
uniform bool uHasAmbientTex;
uniform sampler2D uShininessSampler;
uniform bool uHasShininessTex;

IN vec3 vNormal;
IN vec2 vTexCoord;
Expand Down Expand Up @@ -54,10 +60,16 @@ void main(void) {
// so hooks users don't have to think about premultiplied alpha.
inputs.color.rgb /= inputs.color.a;
}
inputs.shininess = uShininess;
inputs.shininess = uHasShininessTex
? uShininess * TEXTURE(uShininessSampler, vTexCoord).r
: uShininess;
inputs.metalness = uMetallic;
inputs.ambientMaterial = uHasSetAmbient ? uAmbientMatColor.rgb : inputs.color.rgb;
inputs.specularMaterial = uSpecularMatColor.rgb;
inputs.ambientMaterial = uHasAmbientTex
? TEXTURE(uAmbientSampler, vTexCoord).rgb * uAmbientMatColor.rgb
: (uHasSetAmbient ? uAmbientMatColor.rgb : inputs.color.rgb);
inputs.specularMaterial = uHasSpecularTex
? TEXTURE(uSpecularSampler, vTexCoord).rgb * uSpecularMatColor.rgb
: uSpecularMatColor.rgb;
inputs.emissiveMaterial = uEmissiveMatColor.rgb;
inputs = HOOK_getPixelInputs(inputs);

Expand Down
36 changes: 36 additions & 0 deletions test/unit/io/parseMtl.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ suite('parseMtlData', function () {
'map_Kd diffuse.png',
'map_Ka ambient.png',
'map_Ks specular.png',
'map_Ns shininess.png',
'map_Bump -bm 0.5 bump.png'
].join('\n');

Expand All @@ -28,6 +29,7 @@ suite('parseMtlData', function () {
expect(m.texturePath).toEqual('diffuse.png');
expect(m.ambientTexturePath).toEqual('ambient.png');
expect(m.specularTexturePath).toEqual('specular.png');
expect(m.shininessTexturePath).toEqual('shininess.png');
// bump options like -bm precede the path, so the path is the last token.
expect(m.bumpTexturePath).toEqual('bump.png');
});
Expand Down Expand Up @@ -69,4 +71,38 @@ suite('mtlToPartState', function () {
expect(state.fill).toBeNull();
expect(state.texture).toBeNull();
});

test('a specular map lands on the part state with a white base', function () {
const img = { width: 1, height: 1 };
const state = mtlToPartState({ specularTexture: img });
expect(state.specularTexture).toBe(img);
// with no explicit Ks, the base specular colour defaults to white so the
// map has something to modulate
expect(state.specularColor).toEqual([1, 1, 1]);
});

test('a specular map keeps an explicit Ks colour', function () {
const img = { width: 1, height: 1 };
const state = mtlToPartState({
specularColor: [0.5, 0.5, 0.5],
specularTexture: img
});
expect(state.specularTexture).toBe(img);
expect(state.specularColor).toEqual([0.5, 0.5, 0.5]);
});

test('an ambient map lands on the part state with a white base', function () {
const img = { width: 1, height: 1 };
const state = mtlToPartState({ ambientTexture: img });
expect(state.ambientTexture).toBe(img);
expect(state.ambientColor).toEqual([1, 1, 1]);
});

test('a shininess map lands on the part state', function () {
const img = { width: 1, height: 1 };
const state = mtlToPartState({ shininessTexture: img });
expect(state.shininessTexture).toBe(img);
// the map scales a base shininess, which defaults to 1
expect(state.shininess).toEqual(1);
});
});
5 changes: 4 additions & 1 deletion test/unit/webgl/p5.GeometryPart.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ suite('p5.GeometryPart', function () {
ambientColor: null,
specularColor: null,
shininess: null,
texture: null
texture: null,
specularTexture: null,
ambientTexture: null,
shininessTexture: null
});
});

Expand Down
Loading