Skip to content

Conversation

VANSH3104
Copy link
Contributor

Resolves #8074

Changes:

Screenshots of the change:

Screencast.From.2025-09-15.15-30-14.mp4

PR Checklist

@VANSH3104
Copy link
Contributor Author

@davepagurek Could you please take a look at this PR and provide a review? Thanks!

@davepagurek
Copy link
Contributor

Hi! So far this change just reverses the order of the vertices in front faces; I don't think we need to change that. I left a comment here #8074 (comment) explaining the approach I had in mind, which likely involves changing this part of the code that constructs the geometry:

p5.js/src/type/p5.Font.js

Lines 547 to 588 in fc8ca8a

if (extrude === 0) {
const prevValidateFaces = this._pInst._renderer._validateFaces;
this._pInst._renderer._validateFaces = true;
this._pInst.beginShape();
this._pInst.normal(0, 0, 1);
for (const contour of contours) {
this._pInst.beginContour();
for (const { x, y } of contour) {
this._pInst.vertex(x, y);
}
this._pInst.endContour(this._pInst.CLOSE);
}
this._pInst.endShape();
this._pInst._renderer._validateFaces = prevValidateFaces;
} else {
const prevValidateFaces = this._pInst._renderer._validateFaces;
this._pInst._renderer._validateFaces = true;
// Draw front faces
for (const side of [1, -1]) {
this._pInst.beginShape();
for (const contour of contours) {
this._pInst.beginContour();
for (const { x, y } of contour) {
this._pInst.vertex(x, y, side * extrude * 0.5);
}
this._pInst.endContour(this._pInst.CLOSE);
}
this._pInst.endShape();
}
this._pInst._renderer._validateFaces = prevValidateFaces;
// Draw sides
for (const contour of contours) {
this._pInst.beginShape(this._pInst.QUAD_STRIP);
for (const v of contour) {
for (const side of [-1, 1]) {
this._pInst.vertex(v.x, v.y, side * extrude * 0.5);
}
}
this._pInst.endShape();
}

Currently there's an if/else that uses a different approach if extruding vs if creating a flat model. Instead, we'd probably always start with the flat model, but if we need extrusion, you could read the vertices/faces/edges properties of the geometry to manually construct a second extruded geometry.

@VANSH3104
Copy link
Contributor Author

@davepagurek can you review this now its working perfectly i add your recommendation of making a new geometry

Copy link
Contributor

@davepagurek davepagurek left a comment

Choose a reason for hiding this comment

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

This is looking good! I left a few little comments below. I think the big thing left to figure out is the normals. The problem currently is that each side face has its own normal, so the sides will look faceted instead of smooth.

Ideally, I'd like us to be able to just call extruded.computeNormals() and have it generate all the normals for us. It works per vertex by averaging the normals of the faces sharing that vertex. In order to do that, this means:

  • the side faces need to share vertices with adjacent side faces. This means adding all the vertices on each side to extruded.vertices once, and then adding faces that reference the vertex indices for each of those rather than creating brand new vertices in each face.
  • the side faces cannot share any vertices with the front and back faces. This is because if they do, the edge between the front/back and the sides will be smoothed, but we want a crisp edge there.

Let me know if you need any pointers on how to accomplish that!

this._pInst.beginShape();
this._pInst.normal(0, 0, 1);
for (const contour of contours) {
const outer = glyphContours[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

In 2.0, you can surround every contour with begin/endContour including the first one! so you dont have to special case the first contour. How the code used to work will suffice here:

for (const contour of contours) {
  this._pInst.beginContour();
  for (const { x, y } of contour) {
    this._pInst.vertex(x, y);
  }
  this._pInst.endContour(this._pInst.CLOSE);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing this out! I updated the code so every contour uses beginContour/endContour, including the first one.and I originally added stroke(0) with a push/pop to make sure edges are generated consistently.
Following your suggestion, I’ll keep only what’s needed and avoid forcing stroke unless required.

const geom = this._pInst.buildGeometry(() => {
      const prevValidateFaces = this._pInst._renderer._validateFaces;
      this._pInst._renderer._validateFaces = true;
      this._pInst.push();
      this._pInst.stroke(0);

      contours.forEach(glyphContours => {
        this._pInst.beginShape();
        for (const contour of glyphContours) {
          this._pInst.beginContour();
          contour.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
          this._pInst.endContour(this._pInst.CLOSE);
        }
        this._pInst.endShape(this._pInst.CLOSE);
      });
      this._pInst.pop();
      this._pInst._renderer._validateFaces = prevValidateFaces;
    });

i add this adding stroke 0 always and also add push

});

if (extrude === 0) {
console.log('No extrusion');
Copy link
Contributor

Choose a reason for hiding this comment

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

We should take this out before merging so we don't clutter users' console.

extruded.faces = [];

let vertexIndex = 0;
const Vector = this._pInst.constructor.Vector;
Copy link
Contributor

Choose a reason for hiding this comment

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

We can probably just import { Vector } from '../math/p5.Vector'; at the top so that we don't need to import it through here

// Side faces from edges
let edges = geom.edges;
if (!edges || !Array.isArray(edges)) {
edges = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Was there a scenario when edges was empty? We can try temporarily setting stroke(0) (within a push/pop) when generating the initial geometry to ensure that edges are present if we need. I think if we can do that and guarantee the edges exist, we can take out this block of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had a fallback in case edges was empty, but since you mentioned we can guarantee edges by enabling stroke inside a push/pop, I simplified this block and removed the redundant fallback.

const validEdges = geom.edges.filter(([a, b]) => a !== b);

for (const [a, b] of validEdges) {
  const v0 = geom.vertices[a];
  const v1 = geom.vertices[b];

  const vFront0 = new Vector(v0.x, v0.y, v0.z + half);
  const vFront1 = new Vector(v1.x, v1.y, v1.z + half);
  const vBack0 = new Vector(v0.x, v0.y, v0.z - half);
  const vBack1 = new Vector(v1.x, v1.y, v1.z - half);

  extruded.faces.push([vFront0, vBack0, vBack1]);
  extruded.faces.push([vFront0, vBack1, vFront1]);
}

@VANSH3104
Copy link
Contributor Author

@davepagurek The extrusion is still not rendering correctly on the top and bottom faces. In my earlier changes, the top and bottom rendered perfectly, but in this version they only work if I comment out extruded.computeNormals(). It seems that computeNormals() is overriding the normals in a way that breaks the top/bottom faces I think keeping of eeping the front and back faces with their own independent vertices and make the side faces share vertices with each other, so their normals average smoothly.. this is the code

textToModel(str, x, y, width, height, options) {
    ({ width, height, options } = this._parseArgs(width, height, options));
    const extrude = options?.extrude || 0;
    // Step 1: generate glyph contours
    let contours = this.textToContours(str, x, y, width, height, options);
    if (!Array.isArray(contours[0][0])) {
      contours = [contours];
    }

    // Step 2: build base flat geometry
    const geom = this._pInst.buildGeometry(() => {
      const prevValidateFaces = this._pInst._renderer._validateFaces;
      this._pInst._renderer._validateFaces = true;
      this._pInst.push();
      this._pInst.stroke(0);

      contours.forEach(glyphContours => {
        this._pInst.beginShape();
        for (const contour of glyphContours) {
          this._pInst.beginContour();
          contour.forEach(({ x, y }) => this._pInst.vertex(x, y, 0));
          this._pInst.endContour(this._pInst.CLOSE);
        }
        this._pInst.endShape(this._pInst.CLOSE);
      });
      this._pInst.pop();
      this._pInst._renderer._validateFaces = prevValidateFaces;
    });
    if (extrude === 0) {
      return geom;
    }

    // Step 3: Create extruded geometry with UNSHARED vertices for flat shading
    const extruded = this._pInst.buildGeometry(() => {});
    const half = extrude * 0.5;

    extruded.vertices = [];
    extruded.faces = [];

    const frontVertexOffset = 0;
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      extruded.vertices.push(new Vector(v.x, v.y, v.z + half));
    }

    for (const face of geom.faces) {
      if (face.length < 3) continue;
      for (let i = 1; i < face.length - 1; i++) {
        extruded.faces.push([
          face[0] + frontVertexOffset,
          face[i] + frontVertexOffset,
          face[i + 1] + frontVertexOffset
        ]);
      }
    }
    const backVertexOffset = extruded.vertices.length;
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      extruded.vertices.push(new Vector(v.x, v.y, v.z - half));
    }
    for (const face of geom.faces) {
      if (face.length < 3) continue;
      for (let i = 1; i < face.length - 1; i++) {
        const v1 = geom.vertices[face[i]];
        const v2 = geom.vertices[face[i + 1]];
        extruded.faces.push([
          face[0] + backVertexOffset,
          face[i + 1] + backVertexOffset,
          face[i] + backVertexOffset
        ]);
      }
    }
    // Side faces from edges
    const sideVertexOffset = extruded.vertices.length;
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      // Add vertex at front
      extruded.vertices.push(new Vector(v.x, v.y, v.z + half));
    }
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      // Add vertex at back
      extruded.vertices.push(new Vector(v.x, v.y, v.z - half));
    }


    const validEdges = geom.edges.filter(([a, b]) => a !== b);

    for (const [a, b] of validEdges) {
      // Indices for the 4 corners of the side quad
      const frontA = sideVertexOffset + a;
      const frontB = sideVertexOffset + b;
      const backA = sideVertexOffset + geom.vertices.length + a;
      const backB = sideVertexOffset + geom.vertices.length + b;
      
      // Two triangles forming the side quad
      extruded.faces.push([frontA, backA, backB]);
      extruded.faces.push([frontA, backB, frontB]);
    }
    extruded.computeNormals();
    return extruded;
  }

@VANSH3104
Copy link
Contributor Author

VANSH3104 commented Oct 4, 2025

@davepagurek I think the problem could be duplication of normals for the top and bottom faces when using separate side vertices. When we create new vertices for the sides, each side face has its own independent vertices, so computeNormals() cannot average normals across adjacent side faces. This causes the sides to look faceted and can even distort the normals for the top and bottom faces.I also noticed that the extrusion renders correctly if we reuse the existing front and back vertices for the side faces instead of creating new ones.

   //removing sidevertexoffset
    const validEdges = geom.edges.filter(([a, b]) => a !== b);
   //changes here
    for (const [a, b] of validEdges) {
      // Indices for the 4 corners of the side quad
      const frontA = frontVertexOffset + a;
      const frontB = frontVertexOffset + b;
      const backA  = backVertexOffset + a;
      const backB  = backVertexOffset + b;

      // Two triangles forming the side quad
      extruded.faces.push([frontA, backA, backB]);
      extruded.faces.push([frontA, backB, frontB]);
    }
    extruded.computeNormals();
    return extruded;
  }

Am I on the right track? Which approach aligns better?

@davepagurek
Copy link
Contributor

The first approach you mentioned looks right -- we intentionally don't want the front/back faces to share vertices with the sides so that there is a crisp edge. Can you send a screenshot of what the shading look like with that so I get a clearer idea of the problem?

Some thoughts though:

  • computeNormals is sensitive to the order of the vertices in the faces. Right now the front and back faces are in the same order, so we might end up with normals facing in the same direction for the front and back, when we really want them facing in opposite directions. So we might have to reverse the back faces, which we also used to do here

    p5.js/src/type/p5.Font.js

    Lines 594 to 597 in f76b2b6

    if (face.every(idx => geom.vertices[idx].z <= -extrude * 0.5 + 0.1)) {
    for (const idx of face) geom.vertexNormals[idx].set(0, 0, -1);
    face.reverse();
    }

    image

  • There might be some numerical issues with small faces causing issues? we know that the front and back faces should all have normals facing in +/- [0, 0, 1]. So possibly after calling computeNormals() we can also just overwrite the normals at the indices for the front and back faces. In fact, to save some computation, we could actually add just the side faces first, call computeNormals, and then add the front/back faces with manual normals.

@VANSH3104
Copy link
Contributor Author

@davepagurek it looks same as original issue have top and bottom dont render normals properly its really getting way complex triying to figure out now
Screenshot from 2025-10-04 22-12-20
Screenshot from 2025-10-04 22-11-49
Screenshot from 2025-10-04 22-11-35

@VANSH3104
Copy link
Contributor Author

@davepagurek ok so I Initialized vertexNormals array manually and assigned exact normals of [0,0,1] to all front vertices and [0,0,-1] to all back vertices when creating them. This eliminates any numerical issues since we know these faces should be perfectly flat.Reordered the face creation process. Side faces are now added first, then computeNormals() is called which only affects the side vertices since front and back vertices already have their normals set. After that, front and back faces are added to the faces array. This optimization means computeNormals() doesn't waste computation on faces where we already know the exact normals.Reversed the winding order of back faces by swapping face[i] and face[i+1] in the back face loop. This ensures that when viewed from outside the geometry, the back faces have vertices ordered counterclockwise (opposite to the front faces), which is necessary for normals to point in opposite directions.I think that allign to your thoughts still it giving same rendering dont able to figure it now

const extruded = this._pInst.buildGeometry(() => {});
    const half = extrude * 0.5;

    extruded.vertices = [];
    extruded.faces = [];
    extruded.vertexNormals = [];

    const frontVertexOffset = 0;
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      extruded.vertices.push(new Vector(v.x, v.y, v.z + half));
      extruded.vertexNormals.push(new Vector(0, 0, 1));
    }

    const backVertexOffset = extruded.vertices.length;
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      extruded.vertices.push(new Vector(v.x, v.y, v.z - half));
      extruded.vertexNormals.push(new Vector(0, 0, -1));
    }
    // Side faces from edges
    const sideVertexOffset = extruded.vertices.length;
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      // Add vertex at front
      extruded.vertices.push(new Vector(v.x, v.y, v.z + half));
      extruded.vertexNormals.push(new Vector(0, 0, 0)); 
    }
    for (let i = 0; i < geom.vertices.length; i++) {
      const v = geom.vertices[i];
      // Add vertex at back
      extruded.vertices.push(new Vector(v.x, v.y, v.z - half));
      extruded.vertexNormals.push(new Vector(0, 0, 0)); 
    }


    const validEdges = geom.edges.filter(([a, b]) => a !== b);

    for (const [a, b] of validEdges) {
      // Indices for the 4 corners of the side quad
      const frontA = sideVertexOffset + a;
      const frontB = sideVertexOffset + b;
      const backA = sideVertexOffset + geom.vertices.length + a;
      const backB = sideVertexOffset + geom.vertices.length + b;
      
      // Two triangles forming the side quad
      extruded.faces.push([frontA, backA, backB]);
      extruded.faces.push([frontA, backB, frontB]);
    }
    extruded.computeNormals();
   //Add front faces
    for (const face of geom.faces) {
      if (face.length < 3) continue;
      for (let i = 1; i < face.length - 1; i++) {
        extruded.faces.push([
          face[0] + frontVertexOffset,
          face[i] + frontVertexOffset,
          face[i + 1] + frontVertexOffset
        ]);
      }
    }
    // Add back faces with reverse order
    for (const face of geom.faces) {
      if (face.length < 3) continue;
      for (let i = 1; i < face.length - 1; i++) {
        extruded.faces.push([
          face[0] + backVertexOffset,
          face[i] + backVertexOffset,
          face[i + 1] + backVertexOffset
        ]);
      }
    }
    return extruded;
  }

@davepagurek
Copy link
Contributor

So it looks to me like computeNormals() is working properly and the faces are set up well. But seeing your screenshot, it looks like it has the original problem from the original issue where we have multiple contours overlapping. My new theory for why this is happening is that we're constructing the side edges not from the triangulation result, which should flatten the contours into one shape, but from the edges, which works a little differently and may be preserving the original multiple contours. The reason for this is that the edges array generated by p5 is meant to outline the contours you pass in, not the unified resulting shape.

A quick way to test this would be to generate the side edges not from the edges array, but by making a side face for every edge of every front triangle. I suspect that this will make the artifact on the sides go away if this is in fact the problem. If that's true, we won't be done, because it'll also be extruding faces inside the shape, which we'll want to see if we can get rid of somehow, but we can maybe wait and see first if this idea works before worrying about that:
image

If it doesn't fix it, it could mean that the output of triangulation is still giving us multiple contours that overlap, which would mean having to think harder about how to prevent that overlap, possibly with a different library to boolean the shapes together.

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