From a1fcfeb97136e2a48d0fd3a146a5dfa29f9fc724 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 12 Aug 2026 12:17:05 +0530 Subject: [PATCH 1/2] carry custom vertex attributes into multi-material parts --- src/webgl/GeometryBuilder.js | 31 ++++++++++++--- src/webgl/p5.Geometry.js | 69 ++++------------------------------ src/webgl/p5.GeometryPart.js | 73 +++++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 69 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 7f34911bc7..dd73ae7778 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -208,6 +208,27 @@ class GeometryBuilder { for (const f of input.faces) { part.faces.push(f.map(idx => idx + startIdx)); } + + // carry custom per-vertex attributes into the part too, the same way the + // combined geometry does above. draws within a part can use different + // properties, so pad the gaps: props the part already has but this draw + // lacks get zeros for the new verts, and a prop new to the part backfills + // zeros for the verts already in it. + const inputProps = input.userVertexProperties; + const partProps = part.userVertexProperties; + for (const propName in partProps) { + if (propName in inputProps) continue; + const size = partProps[propName].getDataSize(); + partProps[propName].pushDirect(Array(size * vertices.length).fill(0)); + } + for (const propName in inputProps) { + const inProp = inputProps[propName]; + const size = inProp.getDataSize(); + if (startIdx > 0 && !(propName in partProps)) { + part.vertexProperty(propName, Array(size * startIdx).fill(0), size); + } + part.vertexProperty(propName, inProp.getSrcArray(), size); + } } /** @@ -266,13 +287,11 @@ class GeometryBuilder { */ finish() { this.renderer._pInst.pop(); - // expose the material parts only when there really are multiple materials, - // and not while custom per-vertex attributes are in play (those aren't - // split per part yet). single-material builds keep the geometry as its own + // expose the material parts whenever there really are multiple materials. + // custom per-vertex attributes are now carried per part too, so they work + // alongside the split. single-material builds keep the geometry as its own // part, so nothing changes for them (zero regression). - const hasUserProps = - Object.keys(this.geometry.userVertexProperties).length > 0; - if (this.parts.length >= 2 && !hasUserProps) { + if (this.parts.length >= 2) { this.geometry.parts = this.parts; } return this.geometry; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index cd842eaa67..ecde4d75e9 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -8,7 +8,11 @@ import * as constants from '../core/constants'; import { DataArray } from './p5.DataArray'; -import { GeometryPart, createPartState } from './p5.GeometryPart'; +import { + GeometryPart, + createPartState, + createUserVertexProperty +} from './p5.GeometryPart'; import { Vector } from '../math/p5.Vector'; import { downloadFile } from '../io/utilities'; @@ -1814,67 +1818,8 @@ class Geometry { } _userVertexPropertyHelper(propertyName, data, size) { - const geometryInstance = this; - const prop = (this.userVertexProperties[propertyName] = { - name: propertyName, - dataSize: size ? size : data.length ? data.length : 1, - geometry: geometryInstance, - // Getters - getName() { - return this.name; - }, - getCurrentData() { - if (this.currentData === undefined) { - this.currentData = new Array(this.getDataSize()).fill(0); - } - return this.currentData; - }, - getDataSize() { - return this.dataSize; - }, - getSrcName() { - const src = this.name.concat('Src'); - return src; - }, - getDstName() { - const dst = this.name.concat('Buffer'); - return dst; - }, - getSrcArray() { - const srcName = this.getSrcName(); - return this.geometry[srcName]; - }, - //Setters - setCurrentData(data) { - const size = data.length ? data.length : 1; - // if (size != this.getDataSize()){ - // p5._friendlyError(`Custom vertex property '${this.name}' has been set with various data sizes. You can change it's name, or if it was an accident, set '${this.name}' to have the same number of inputs each time!`, 'vertexProperty()'); - // } - this.currentData = data; - }, - // Utilities - pushCurrentData() { - const data = this.getCurrentData(); - this.pushDirect(data); - }, - pushDirect(data) { - if (data.length) { - this.getSrcArray().push(...data); - } else { - this.getSrcArray().push(data); - } - }, - resetSrcArray() { - this.geometry[this.getSrcName()] = []; - }, - delete() { - const srcName = this.getSrcName(); - delete this.geometry[srcName]; - delete this; - } - }); - this[prop.getSrcName()] = []; - return this.userVertexProperties[propertyName]; + // shared with GeometryPart so parts carry custom attributes identically + return createUserVertexProperty(this, propertyName, data, size); } } diff --git a/src/webgl/p5.GeometryPart.js b/src/webgl/p5.GeometryPart.js index 5873ac8959..93f4fa6665 100644 --- a/src/webgl/p5.GeometryPart.js +++ b/src/webgl/p5.GeometryPart.js @@ -20,6 +20,62 @@ function createPartState() { }; } +// build a custom vertex-property accessor bound to `owner` (a p5.Geometry or a +// GeometryPart). the raw data lives on owner[name + 'Src'] and the renderer +// reads it back through getSrcArray()/getDataSize(). shared between geometries +// and parts so a per-material part carries custom attributes the same way the +// whole geometry does. +function createUserVertexProperty(owner, propertyName, data, size) { + const prop = (owner.userVertexProperties[propertyName] = { + name: propertyName, + dataSize: size ? size : data.length ? data.length : 1, + geometry: owner, + getName() { + return this.name; + }, + getCurrentData() { + if (this.currentData === undefined) { + this.currentData = new Array(this.getDataSize()).fill(0); + } + return this.currentData; + }, + getDataSize() { + return this.dataSize; + }, + getSrcName() { + return this.name.concat('Src'); + }, + getDstName() { + return this.name.concat('Buffer'); + }, + getSrcArray() { + return this.geometry[this.getSrcName()]; + }, + setCurrentData(data) { + this.currentData = data; + }, + pushCurrentData() { + this.pushDirect(this.getCurrentData()); + }, + pushDirect(data) { + if (data.length) { + this.getSrcArray().push(...data); + } else { + this.getSrcArray().push(data); + } + }, + resetSrcArray() { + this.geometry[this.getSrcName()] = []; + }, + delete() { + delete this.geometry[this.getSrcName()]; + delete this; + } + }); + owner[prop.getSrcName()] = []; + return owner.userVertexProperties[propertyName]; +} + // one part of a geometry. a multi-material model is a p5.Geometry made of // several parts, each holding the verts/faces/uvs for one material plus the // state to draw them. single-material models are just one part. @@ -42,6 +98,21 @@ class GeometryPart { this.userVertexProperties = {}; } + // append custom per-vertex attribute data to this part, same shape as + // p5.Geometry.vertexProperty so the renderer binds it identically. + vertexProperty(propertyName, data, size) { + let prop = this.userVertexProperties[propertyName]; + if (!prop) { + prop = createUserVertexProperty(this, propertyName, data, size); + } + if (size) { + prop.pushDirect(data); + } else { + prop.setCurrentData(data); + prop.pushCurrentData(); + } + } + // the renderer needs this to pick a blend mode. a part is transparent if its // fill has alpha below 1, or any of its vertex colors does. hasFillTransparency() { @@ -54,4 +125,4 @@ class GeometryPart { } } -export { GeometryPart, createPartState }; +export { GeometryPart, createPartState, createUserVertexProperty }; From cf3cab714ad783f07981acf12ea1cdd8d77bc02f Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 12 Aug 2026 12:17:06 +0530 Subject: [PATCH 2/2] test custom attributes survive the multi-material split --- test/unit/webgl/p5.Geometry.js | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/unit/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js index 85631d8f91..7c3ab813c4 100644 --- a/test/unit/webgl/p5.Geometry.js +++ b/test/unit/webgl/p5.Geometry.js @@ -355,6 +355,50 @@ suite('p5.Geometry', function () { .not.toEqual(geom.parts[1].partState.texture); }); + test('custom vertex attributes survive the multi-material part split', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + const texA = myp5.createGraphics(10, 10); + const texB = myp5.createGraphics(10, 10); + const geom = myp5.buildGeometry(() => { + // first material: aCustom values in 1..3 + myp5.texture(texA); + myp5.beginShape(myp5.TRIANGLES); + myp5.vertexProperty('aCustom', 1); + myp5.vertex(-10, -10, 0); + myp5.vertexProperty('aCustom', 2); + myp5.vertex(10, -10, 0); + myp5.vertexProperty('aCustom', 3); + myp5.vertex(0, 10, 0); + myp5.endShape(); + // second material: aCustom values in 4..6 + myp5.texture(texB); + myp5.beginShape(myp5.TRIANGLES); + myp5.vertexProperty('aCustom', 4); + myp5.vertex(-10, -10, 0); + myp5.vertexProperty('aCustom', 5); + myp5.vertex(10, -10, 0); + myp5.vertexProperty('aCustom', 6); + myp5.vertex(0, 10, 0); + myp5.endShape(); + }); + + // the texture change still splits into two parts even with a custom attr + expect(geom.parts.length).toEqual(2); + + const a0 = geom.parts[0].userVertexProperties.aCustom; + const a1 = geom.parts[1].userVertexProperties.aCustom; + expect(a0).toBeTruthy(); + expect(a1).toBeTruthy(); + // one value per vertex, aligned to each part + expect(a0.getSrcArray().length).toEqual(geom.parts[0].vertices.length); + expect(a1.getSrcArray().length).toEqual(geom.parts[1].vertices.length); + // and each part only carries its own draw's values + expect(a0.getSrcArray().every(v => v >= 1 && v <= 3)).toBe(true); + expect(a1.getSrcArray().every(v => v >= 4 && v <= 6)).toBe(true); + } + ); + test('a fill change alone does not split the build', function() { myp5.createCanvas(50, 50, myp5.WEBGL); const geom = myp5.buildGeometry(() => {