Skip to content
48 changes: 47 additions & 1 deletion src/core/p5.Renderer3D.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,28 @@ export class Renderer3D extends Renderer {
geometry.vertices.length >= 3 &&
![constants.LINES, constants.POINTS].includes(mode)
) {
this._drawFills(geometry, { mode, count });
// draw every part. a part with no material state draws straight (no
// push/pop); a single-material geometry is its own part, so that case is
// exactly the old single draw. multi-material parts each apply their own
// material around the draw.
const parts = geometry.parts && geometry.parts.length
? geometry.parts
: [geometry];
for (const part of parts) {
const state = part.partState;
const hasMaterial = state && (
state.fill || state.texture || state.ambientColor ||
state.specularColor || state.shininess != null
);
if (hasMaterial) {
this.push();
this._applyPartState(state);
this._drawFills(part, { mode, count });
this.pop();
} else {
this._drawFills(part, { mode, count });
}
}
}

if (this.states.strokeColor && geometry.lineVertices.length >= 1) {
Expand Down Expand Up @@ -628,6 +649,31 @@ export class Renderer3D extends Renderer {
shader.unbindShader();
}

// apply a part's material to the renderer before it's drawn. only non-null
// fields are set, so an empty part state leaves the uniforms untouched.
_applyPartState(partState) {
if (!partState) return;
if (partState.fill) {
const c = partState.fill;
this.states.setValue('curFillColor', [c[0], c[1], c[2], 1]);
}
if (partState.texture) {
this.states.setValue('_tex', partState.texture);
this.states.setValue('drawMode', constants.TEXTURE);
}
if (partState.ambientColor) {
this.states.setValue('curAmbientColor', partState.ambientColor);
this.states.setValue('_hasSetAmbient', true);
}
if (partState.specularColor) {
this.states.setValue('curSpecularColor', partState.specularColor);
this.states.setValue('_useSpecularMaterial', true);
}
if (partState.shininess != null) {
this.states.setValue('_useShininess', partState.shininess);
}
}

_drawStrokes(geometry, { count } = {}) {

this._useLineColor = geometry.vertexStrokeColors.length > 0;
Expand Down
2 changes: 2 additions & 0 deletions src/webgl/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import renderBuffer from './p5.RenderBuffer';
import quat from './p5.Quat';
import matrix from '../math/p5.Matrix';
import geometry from './p5.Geometry';
import geometryPart from './p5.GeometryPart';
import framebuffer from './p5.Framebuffer';
import dataArray from './p5.DataArray';
import camera from './p5.Camera';
Expand All @@ -28,6 +29,7 @@ export default function(p5){
p5.registerAddon(quat);
p5.registerAddon(matrix);
p5.registerAddon(geometry);
p5.registerAddon(geometryPart);
p5.registerAddon(camera);
p5.registerAddon(framebuffer);
p5.registerAddon(dataArray);
Expand Down
199 changes: 159 additions & 40 deletions src/webgl/loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { Geometry } from './p5.Geometry';
import { GeometryPart, createPartState } from './p5.GeometryPart';
import { Vector } from '../math/p5.Vector';
import { request } from '../io/files';

Expand All @@ -17,6 +18,155 @@ async function fileExists(url) {
}
}

// parse mtl text into a map of material name -> props. split from the file
// request so it's testable on its own.
function parseMtlData(data) {
let currentMaterial = null;
const materials = {};
const lines = data.split('\n');

for (let line = 0; line < lines.length; ++line) {
const tokens = lines[line].trim().split(/\s+/);
if (tokens[0] === 'newmtl') {
currentMaterial = tokens[1];
materials[currentMaterial] = {};
} else if (!currentMaterial) {
continue;
} else if (tokens[0] === 'Kd') {
//diffuse color
materials[currentMaterial].diffuseColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'Ka') {
//ambient color
materials[currentMaterial].ambientColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'Ks') {
//specular color
materials[currentMaterial].specularColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'Ns') {
//specular exponent (shininess)
materials[currentMaterial].shininess = parseFloat(tokens[1]);
} else if (tokens[0] === 'd') {
//dissolve, 1 is fully opaque
materials[currentMaterial].opacity = parseFloat(tokens[1]);
} else if (tokens[0] === 'Tr') {
//transparency, the inverse of d
materials[currentMaterial].opacity = 1 - parseFloat(tokens[1]);
} else if (tokens[0] === 'illum') {
//illumination model
materials[currentMaterial].illuminationModel = parseInt(tokens[1]);
} else if (tokens[0] === 'map_Kd') {
//diffuse texture
materials[currentMaterial].texturePath = tokens[1];
} else if (tokens[0] === 'map_Ka') {
//ambient texture
materials[currentMaterial].ambientTexturePath = tokens[1];
} else if (tokens[0] === 'map_Ks') {
//specular texture
materials[currentMaterial].specularTexturePath = 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.
materials[currentMaterial].bumpTexturePath = tokens[tokens.length - 1];
}
}

return materials;
}

// mtl material -> part state in p5's vocab. anything we can't draw yet is left
// off until support lands.
function mtlToPartState(material) {
const state = createPartState();
if (!material) return state;
if (material.diffuseColor) state.fill = material.diffuseColor;
if (material.ambientColor) state.ambientColor = material.ambientColor;
if (material.specularColor) state.specularColor = material.specularColor;
if (material.shininess !== undefined) state.shininess = material.shininess;
if (material.texture) state.texture = material.texture;
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.
async function loadMaterialTextures(materials, modelPath, instance) {
if (!instance || typeof instance.loadImage !== 'function') return;

const slash = modelPath.lastIndexOf('/');
const folder = slash >= 0 ? modelPath.slice(0, slash) : '';
const resolve = file => (folder ? `${folder}/${file}` : file);

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}`);
})
);
}

await Promise.all(jobs);
}

// split the model's faces into one part per material. the combined arrays stay
// as the aggregate; each part gets its own localised verts with faces re-indexed
// against them, plus its material's state.
function buildMaterialParts(model, faceMaterials, materials) {
// only split when there are genuinely multiple materials. a single material
// (or none) stays as the geometry's own part and renders as before. one group
// per material, plus a null group for faces before any usemtl so none drop.
const names = [...new Set(faceMaterials)];
if (names.filter(name => name != null).length < 2) return;

const hasUvs = model.uvs.length > 0;
const hasNormals = model.vertexNormals.length > 0;
const parts = [];

for (const name of names) {
const part = new GeometryPart(
`${model.gid}|part${parts.length}`,
mtlToPartState(materials[name])
);
// global vertex index -> this part's local index, added on first use
const localIndex = new Map();
for (let fi = 0; fi < model.faces.length; fi++) {
if (faceMaterials[fi] !== name) continue;
const localFace = model.faces[fi].map(vi => {
if (!localIndex.has(vi)) {
localIndex.set(vi, part.vertices.length);
part.vertices.push(model.vertices[vi]);
if (hasUvs) part.uvs.push(model.uvs[vi]);
if (hasNormals) part.vertexNormals.push(model.vertexNormals[vi]);
}
return localIndex.get(vi);
});
part.faces.push(localFace);
}
parts.push(part);
}

model.parts = parts;
}

function loading(p5, fn){
/**
* Loads a 3D model to create a
Expand Down Expand Up @@ -446,6 +596,7 @@ function loading(p5, fn){
const lines = data.split('\n');

const parsedMaterials = await getMaterials(lines);
await loadMaterialTextures(parsedMaterials, path, this);
const cb = () => {
parseObj(model, lines, parsedMaterials);

Expand Down Expand Up @@ -482,47 +633,8 @@ function loading(p5, fn){
* @private
*/
async function parseMtl(mtlPath) {
let currentMaterial = null;
let materials = {};

const { data } = await request(mtlPath, 'text');
const lines = data.split('\n');

for (let line = 0; line < lines.length; ++line) {
const tokens = lines[line].trim().split(/\s+/);
if (tokens[0] === 'newmtl') {
const materialName = tokens[1];
currentMaterial = materialName;
materials[currentMaterial] = {};
} else if (tokens[0] === 'Kd') {
//Diffuse color
materials[currentMaterial].diffuseColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'Ka') {
//Ambient Color
materials[currentMaterial].ambientColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];
} else if (tokens[0] === 'Ks') {
//Specular color
materials[currentMaterial].specularColor = [
parseFloat(tokens[1]),
parseFloat(tokens[2]),
parseFloat(tokens[3])
];

} else if (tokens[0] === 'map_Kd') {
//Texture path
materials[currentMaterial].texturePath = tokens[1];
}
}

return materials;
return parseMtlData(data);
}

/**
Expand Down Expand Up @@ -557,6 +669,8 @@ function loading(p5, fn){
// Map from source index → Map of material → destination index
const usedVerts = {}; // Track colored vertices
let currentMaterial = null;
// material per kept face, aligned with model.faces, for bucketing later
const faceMaterials = [];
let hasColoredVertices = false;
let hasColorlessVertices = false;
for (let line = 0; line < lines.length; ++line) {
Expand Down Expand Up @@ -642,6 +756,7 @@ function loading(p5, fn){
face[1] !== face[2]
) {
model.faces.push(face);
faceMaterials.push(currentMaterial);
}
}
}
Expand All @@ -655,6 +770,9 @@ function loading(p5, fn){
model.vertexColors = [];
}

// bucket faces into per-material parts (aggregate arrays above stay as-is)
buildMaterialParts(model, faceMaterials, materials);

return model;
}

Expand Down Expand Up @@ -1296,6 +1414,7 @@ function loading(p5, fn){
}

export default loading;
export { parseMtlData, mtlToPartState, buildMaterialParts };

if(typeof p5 !== 'undefined'){
loading(p5, p5.prototype);
Expand Down
Loading
Loading