Skip to content

GSOC 26: render multi-material models per part#8955

Open
Nixxx19 wants to merge 14 commits into
processing:mainfrom
Nixxx19:renderer-per-part
Open

GSOC 26: render multi-material models per part#8955
Nixxx19 wants to merge 14 commits into
processing:mainfrom
Nixxx19:renderer-per-part

Conversation

@Nixxx19

@Nixxx19 Nixxx19 commented Jun 24, 2026

Copy link
Copy Markdown
Member

part of my google summer of code 2026 project, "full texture support for .mtl files in p5.js". works toward #6924.

this is phase 3: the renderer side, building on the data + parser foundation in #8879. multi-material models now actually draw, one part per material, each with its own look. public api is unchanged, you still just call loadModel() and model().

project phases for context:

what's in here:

  • the renderer loops over a geometry's parts and draws each one
  • each part applies its own material (fill, specular, ambient, shininess, texture) around its draw, restored after so nothing leaks between parts
  • a single-material geometry is its own part, so that path renders exactly as before, byte for byte
  • p5.GeometryPart gains what the renderer needs to treat it as a drawable (hasFillTransparency, userVertexProperties)

tests:

  • a multi-material model (octa-color, 8 materials) draws one fill per part and passes the instance count through
  • the existing obj+mtl diffuse-colour visual test still matches its reference screenshot
  • full unit suite green, including the webgpu and visual (pixel-compare) tests

stacked on #8879, so it should go in after that one merges.

@Nixxx19 Nixxx19 changed the title render multi-material models per part GSOC 26: render multi-material models per part Jun 24, 2026

@davepagurek davepagurek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking good! I left a few comments. One thing that would be good to clarify is what the valid states of a geometry are (should they always have parts?) so that that can be an invariant in the code and simplifies implementation.

Comment thread src/core/p5.Renderer3D.js Outdated
// material around the draw.
const parts = geometry.parts && geometry.parts.length
? geometry.parts
: [geometry];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's a scenario where we'd end up with no parts?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

there isn't one. the constructor always sets parts = [this] and loaders replace it, so a geometry always has at least one part. i'll make that an invariant and just loop geometry.parts directly, dropping the fallback

Comment thread src/core/p5.Renderer3D.js Outdated
if (!partState) return;
if (partState.fill) {
const c = partState.fill;
this.states.setValue('curFillColor', [c[0], c[1], c[2], 1]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason why it's not possible for a part to have alpha in the fill? Although the obj/mtl spec puts transparency into its own command, in p5 fills can have alpha (e.g. geometry that comes from buildGeometry would be able to have fills with alpha if a user calls something like fill(255, 100) in the middle of building it.) Maybe we can store the transparency from a mtl in the alpha field of its fill color?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yeah that makes sense. fills carry alpha in p5, so i'll store the mtl d/Tr transparency in the fill colour's alpha (4th component) instead of forcing 1. that also covers the buildGeometry fill(255,100) case. nice, this brings transparency back in the right place

Comment thread test/unit/webgl/p5.GeometryPart.js Outdated
suite('multi-material rendering', function() {
test('draws each part and passes the instance count through', async function() {
const renderer = myp5.createCanvas(50, 50, myp5.WEBGL);
const model = await new Promise(resolve =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think loadModel(url) should already returns a promise, without needing to wrap it further

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

right, will simplify to const model = await myp5.loadModel('/test/unit/assets/octa-color.obj') and drop the wrapper

Comment thread test/unit/webgl/p5.GeometryPart.js Outdated

const spy = vi.spyOn(renderer, '_drawFills');
myp5.background(255);
myp5.model(model, 4);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The second parameter here is for WebGL instanced rendering, is that intended to be set in this test?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the 4 was to confirm the instance count reaches each part's draw, but you're right it muddies this test, i'll split it, assert per-part drawing on its own and keep the instancing check separate

Comment thread src/webgl/p5.GeometryPart.js Outdated
}

function geometryPart(p5, fn) {
p5.GeometryPart = GeometryPart;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this maybe could be done in the existing geometry addon, since there's not a way to use geometry without geometryPart currently. So this file could just export the class, and p5.Geometry can import it and add it to the p5 object

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

agreed, no reason for a separate addon. i'll have p5.GeometryPart.js just export the class and let p5.Geometry import it and add it to the p5 object, dropping the extra registerAddon

Comment thread test/unit/webgl/p5.GeometryPart.js Outdated
// octa-color has several materials, so several parts.
expect(model.parts.length).toBeGreaterThan(1);

const spy = vi.spyOn(renderer, '_drawFills');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should have a similar spy and assertion for _drawStrokes, which I believe we still need to implement.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

good catch, strokes aren't per-part yet, only fills. i'll add per-part stroke drawing and a _drawStrokes spy + assertion alongside the fills one

@Nixxx19

Nixxx19 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

Looking good! I left a few comments. One thing that would be good to clarify is what the valid states of a geometry are (should they always have parts?) so that that can be an invariant in the code and simplifies implementation.

thanks for the review! on the invariant, yeah a geometry should always have at least one part, the constructor guarantees it (parts = [this]) and loaders replace it. i'll lean into that so the renderer just loops parts with no fallback, like you said, addressing the rest now

@Nixxx19

Nixxx19 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

heads up, the failing check is just flaky ci, not anything in this pr.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants