Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow vertex-wise coloring of areas bounded by bezier and quadratic curves in webgl #5919

Closed
2 of 17 tasks
inaridarkfox4231 opened this issue Dec 27, 2022 · 9 comments · Fixed by #5920
Closed
2 of 17 tasks

Comments

@inaridarkfox4231
Copy link
Contributor

Increasing Access

Currently, in webgl, when the fill function is called immediately before the vertex function in immediateMode to determine the color, each vertex is colored and a gradation is applied.
However, doing this for the bezierVertex and quadraticVertex functions would apply the last color set to all vertices added, resulting in nearly identical colors.
This can also be considered one of the uses, but if so, I thought it would be better to make them all the same color.
So, as the default behavior, I thought it would be better to apply interpolation with the color set by the fill function called before the previous vertex, bezierVertex, quadraticVertex function for each vertex.
for example...

bezierSample_Fill

function setup() {
  createCanvas(400, 400, WEBGL);

  beginShape();

  fill(255);
  vertex(0, -200);

  fill(255, 0, 0);
  bezierVertex(200, -200, 200, 0, 0, 0);

  fill(0, 0, 255);
  bezierVertex(-200, 0, -200, 200, 0, 200);

  endShape();
}

bezierSample_Fill_compare

quadraticSample_Fill

function setup() {
  createCanvas(400, 400, WEBGL);

  beginShape();

  fill(255);
  vertex(0, -200);

  fill(255, 0, 0);
  quadraticVertex(200, 0, 0, 0);

  fill(0, 0, 255);
  quadraticVertex(-200, 0, 0, 200);

  endShape();
}

quadraticSample_Fill_compare

My claim is that I want to modify the bezierVertex and quadraticVertex functions in webgl to look like this.

Most appropriate sub-area of p5.js?

  • Accessibility
  • Color
  • Core/Environment/Rendering
  • Data
  • DOM
  • Events
  • Image
  • IO
  • Math
  • Typography
  • Utilities
  • WebGL
  • Build Process
  • Unit Testing
  • Internalization
  • Friendly Errors
  • Other (specify if possible)

Feature request details

Make changes in the following locations:
main/src/webgl/3d_primitives.js
line1552: bezierVertex
line1643: quadraticVertex

Both the bezierVertex function and the quadraticVertex function have the restriction that the vertex function must be called first, so immediateMode.geometry.vertexColors definitely contains some color.
Therefore, I thought that it would be better to take the last color stored in the array as the start point, set the current color as the end point, and interpolate between them to determine the intermediate color.

First, just after setting the LUTLength, get the start and end colors.

// Get the last stored color and the current color
const _vertexColors = this.immediateMode.geometry.vertexColors;
const _vertexColorSize = _vertexColors.length;
const previousFillColor = [
  _vertexColors[_vertexColorSize - 4],
  _vertexColors[_vertexColorSize - 3],
  _vertexColors[_vertexColorSize - 2],
  _vertexColors[_vertexColorSize - 1]
];
const fillColor = this.curFillColor.slice();

Then, in a for loop, calculate the intermediate colors by interpolating these colors by ratio. Assign this to curFillColor each time so it gets applied in the vertex function.

for (i = 0; i < LUTLength; i++) {
  // Interpolate between previous color and current color
  const ratio = i/LUTLength;
  this.curFillColor = [
    previousFillColor[0] * (1-ratio) + fillColor[0] * ratio,
    previousFillColor[1] * (1-ratio) + fillColor[1] * ratio,
    previousFillColor[2] * (1-ratio) + fillColor[2] * ratio,
    previousFillColor[3] * (1-ratio) + fillColor[3] * ratio
  ];

Finally, after the loop finishes, we reset the current color to curFillColor, just in case.

this.curFillColor = fillColor;

Do this for both the 6 and 9 argument cases.
I would like to apply these changes to both the bezierVertex and quadraticVertex functions.

@inaridarkfox4231
Copy link
Contributor Author

Oh, sorry, 6 and 9 for the bezierVertex function, but 4 and 6 for the quadraticVertex function.

@inaridarkfox4231
Copy link
Contributor Author

benchMark test.
OpenProcessing
p5.Editor
benchMark_bezierFill
Although there are some parts that fall slightly, it seemed to maintain almost 60fps.
Since tessellation is executed every frame, even if the specifications were just to remove the friendlyError caused by TESS (which would make the processing very heavy), I don't think there would be much of a difference.

@davepagurek
Copy link
Contributor

That warning will be fixed in the next p5 version after this PR 🙂 #5910

I think as a default behaviour, this makes sense in general, but rather than linearly interpolating over the length of the look up table, does it make more sense to assign a fill color to each control point, and interpolate the fill color the same way we interpolate position? We have an open issue for how, in the future, one might be able to specify texture coordinates for bezier vertices #5722, and one could in the future apply a fill() between each of those, e.g.:

fill(color1)
vertex(x1, y1, z1, u1, v1)
fill(color2)
bezierControlPoint(x2, y2, z2, u2, v2)
fill(color3)
bezierControlPoint(x3, y3, z3, u3, v3)
fill(color4)
bezierControlPoint(x4, y4, z4, u4, v4)

We don't have to add new methods for all this yet, but in order to support this in the future, I think it might make sense to change the interpolation so that we pick defaults for each control point. The values we pick could use the same logic you use here, interpolating from start to end, since I think as a default the results look good!

@inaridarkfox4231
Copy link
Contributor Author

Thanks for reply! Ah, I see, the unevenness in the appearance is due to the poor interpolation method... I think I need to think about it a little more. Thank you!('ω')

@davepagurek
Copy link
Contributor

I think your method is reasonable too! I'm mostly just trying to make sure we build it in a way that will work with other future methods for bezier control points. (Which we don't need to do along with this change, but it could be a cool follow up project if you're interested!)

@inaridarkfox4231
Copy link
Contributor Author

thank you. It's difficult to decide exactly how to interpolate...Actually, there are opinions that this method is fine and should be implemented.

https://twitter.com/mary_s_ann/status/1607606663930073089
translation: It's perfect, it's beautiful! I wanted to add a gradation to something that uses Bezier. If you can do it, I'm sure many coders will be delighted.

So, in my opinion, it's better to implement it in this direction for the time being and postpone the issue of a more accurate implementation.

@davepagurek
Copy link
Contributor

This method is definitely OK too! If we plan on changing it in the future though, it might be a bit better for users if we try to write it in a way that won't change their visuals. That said, we shouldn't let that get in the way of giving people the ability to add colour to beziers at all! So I'll describe what I was thinking, and if you are the one making a PR with the changes, you can decide if that seems feasible, and I'm happy to accept any progress 🙂

Right now the x and y positions along a bezier segment are calculated here:

_x =
w_x[0] * this._lookUpTableBezier[i][0] +
w_x[1] * this._lookUpTableBezier[i][1] +
w_x[2] * this._lookUpTableBezier[i][2] +
w_x[3] * this._lookUpTableBezier[i][3];
_y =
w_y[0] * this._lookUpTableBezier[i][0] +
w_y[1] * this._lookUpTableBezier[i][1] +
w_y[2] * this._lookUpTableBezier[i][2] +
w_y[3] * this._lookUpTableBezier[i][3];

In this, w_x[0] is the first endpoint's x, w_x[3] is the last endpoint's x, and the others are the two control points in between. We could do something similar and make w_r, w_g, w_b, and w_a for the color, where for each, [0] corresponds to the previous fill, [3] corresponds to the final fill, and until we add methods to specify the control point colors, we pick in between values for [1] and [2]. Probably a good default would be to take the length of the bezier hull and use the length along that hull to lerp, something like:

const totalLength = c0.dist(c1) + c1.dist(c2) + c2.dist(c3);
const c1Color = lerpColor(c0Color, c3Color, c0.dist(c1) / totalLength);
const c2Color = lerpColor(c0Color, c3Color, 1 - c2.dist(c3) / totalLength);
const colors = [c0Color, c1Color, c2Color, c3Color]
w_r = colors.map((c) => red(c));
_r = 
   w_r[0] * this._lookUpTableBezier[i][0] + 
   w_r[1] * this._lookUpTableBezier[i][1] + 
   w_r[2] * this._lookUpTableBezier[i][2] + 
   w_r[3] * this._lookUpTableBezier[i][3];
// etc

...and then in the future we can let people specify fill colors instead of using the default interpolation if they choose.

Anyway, if we don't end up doing that now, that's fine too! Like you said, your example images look great as-is. We can just make an issue to add support for it later.

@inaridarkfox4231
Copy link
Contributor Author

Thank you for your suggestion! I thought that if I put a color on the control point, I could use Bezier coefficients, so it would be more accurate, but I couldn't come up with a good idea.
I implemented it for the time being. First the two intermediate colors are calculated from the distance between the control points as follows:

let d0 = Math.sqrt(Math.pow(w_x[0]-w_x[1],2) + Math.pow(w_y[0]-w_y[1],2));
let d1 = Math.sqrt(Math.pow(w_x[1]-w_x[2],2) + Math.pow(w_y[1]-w_y[2],2));
let d2 = Math.sqrt(Math.pow(w_x[2]-w_x[3],2) + Math.pow(w_y[2]-w_y[3],2));
const totalLength = d0 + d1 + d2;
d0 /= totalLength;
d2 /= totalLength;
const firstFillColor = [];
const secondFillColor = [];
for (i = 0; i < 4; i++) {
  firstFillColor.push(previousFillColor[i] * (1-d0) + finalFillColor[i] * d0);
  secondFillColor.push(previousFillColor[i] * d2 + finalFillColor[i] * (1-d2));
}

I decided to call it finalFillColor instead of fillColor for clarity.
Then, based on these colors, we calculate curFillColor using Bezier coefficients in a for loop.

for (i = 0; i < LUTLength; i++) {
  // lerp color.
  this.curFillColor = [0, 0, 0, 0];
  for (k = 0; k < 4; k++) {
    this.curFillColor[k] += this._lookUpTableBezier[i][0] * previousFillColor[k];
    this.curFillColor[k] += this._lookUpTableBezier[i][1] * firstFillColor[k];
    this.curFillColor[k] += this._lookUpTableBezier[i][2] * secondFillColor[k];
    this.curFillColor[k] += this._lookUpTableBezier[i][3] * finalFillColor[k];
  }

Do the same with the 9 variables.
The process of returning curFillColor to finalFillColor after the loop ends is the same.
For quadraticVertex function, also computes intermediate colors in the same way.

let d0 = Math.sqrt(Math.pow(w_x[0]-w_x[1],2) + Math.pow(w_y[0]-w_y[1],2));
let d1 = Math.sqrt(Math.pow(w_x[1]-w_x[2],2) + Math.pow(w_y[1]-w_y[2],2));
const totalLength = d0 + d1;
d0 /= totalLength;
const middleFillColor = [];
for (i = 0; i < 4; i++) {
  middleFillColor.push(previousFillColor[i] * (1-d0) + finalFillColor[i] * d0);
}

I did a comparison and it didn't make much of a difference.
bezierColor
There was no difference in speed. However, I feel that using control points is better considering symmetry when the order of vertices is reversed, so I will implement it this way. I would like to create a pull request, is that okay?

@davepagurek
Copy link
Contributor

Thanks for making that side by side comparison! I'll assign the issue to you and take a look at your PR soon.

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

Successfully merging a pull request may close this issue.

2 participants