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
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