Add pathDetailThreshold for Graphics rendering.

This commit is contained in:
Ben Richards 2024-06-25 15:24:31 +12:00
parent bd54cbb965
commit 6ec1bed898
7 changed files with 198 additions and 68 deletions

View file

@ -395,6 +395,11 @@ var Config = new Class({
*/
this.roundPixels = GetValue(renderConfig, 'roundPixels', true, config);
/**
* @const {number} Phaser.Core.Config#pathDetailThreshold - Threshold for combining points into a single path in the WebGL renderer for Graphics objects. This can be overridden at the Graphics object level.
*/
this.pathDetailThreshold = GetValue(renderConfig, 'pathDetailThreshold', 1, config);
/**
* @const {boolean} Phaser.Core.Config#pixelArt - Prevent pixel art from becoming blurred when scaled. It will remain crisp (tells the WebGL renderer to automatically create textures using a linear filter mode).
*/

View file

@ -7,6 +7,7 @@
* @property {boolean} [desynchronized=false] - When set to `true` it will create a desynchronized context for both 2D and WebGL. See https://developers.google.com/web/updates/2019/05/desynchronized for details.
* @property {boolean} [pixelArt=false] - Sets `antialias` to false and `roundPixels` to true. This is the best setting for pixel-art games.
* @property {boolean} [roundPixels=true] - Draw texture-based Game Objects at only whole-integer positions. Game Objects without textures, like Graphics, ignore this property.
* @property {number} [pathDetailThreshold=1] - Threshold for combining points into a single path in the WebGL renderer for Graphics objects. This can be overridden at the Graphics object level.
* @property {boolean} [transparent=false] - Whether the game canvas will be transparent. Boolean that indicates if the canvas contains an alpha channel. If set to false, the browser now knows that the backdrop is always opaque, which can speed up drawing of transparent content and images.
* @property {boolean} [clearBeforeRender=true] - Whether the game canvas will be cleared between each rendering frame.
* @property {boolean} [preserveDrawingBuffer=false] - If the value is true the WebGL buffers will not be cleared and will preserve their values until cleared or overwritten by the author.

View file

@ -207,6 +207,27 @@ var Graphics = new Class({
*/
this._lineWidth = 1;
/**
* Path detail threshold for the WebGL renderer, in pixels.
* Path segments will be combined until the path is complete
* or the segment length is above the threshold.
*
* If the value is negative, the threshold will be taken from the
* game config `render.pathDetailThreshold` property.
*
* This threshold can greatly improve performance on complex shapes.
* It is calculated at render time and does not affect the original
* path data.
* The threshold is evaluated in screen pixels, so if the object is
* scaled up, fine detail will emerge.
*
* @name Phaser.GameObjects.Graphics#pathDetailThreshold
* @type {number}
* @default -1
* @since 3.90.0
*/
this.pathDetailThreshold = -1;
this.lineStyle(1, 0, 0);
this.fillStyle(0, 0);

View file

@ -84,6 +84,12 @@ var GraphicsWebGLRenderer = function (renderer, src, drawingContext, parentMatri
var commands = src.commandBuffer;
var alpha = camera.alpha * src.alpha;
var pathDetailThreshold = Math.max(
src.pathDetailThreshold,
renderer.config.pathDetailThreshold,
0
);
var lineWidth = 1;
var tx = 0;
@ -128,10 +134,10 @@ var GraphicsWebGLRenderer = function (renderer, src, drawingContext, parentMatri
case Commands.FILL_PATH:
{
calcMatrix.multiply(currentMatrix, renderMatrix);
for (pathIndex = 0; pathIndex < path.length; pathIndex++)
{
calcMatrix.multiply(currentMatrix, renderMatrix);
(customRenderNodes.FillPath || defaultRenderNodes.FillPath).run(
currentContext,
renderMatrix,
@ -139,7 +145,8 @@ var GraphicsWebGLRenderer = function (renderer, src, drawingContext, parentMatri
path[pathIndex].points,
fillTint.TL,
fillTint.TR,
fillTint.BL
fillTint.BL,
pathDetailThreshold
);
}
break;
@ -147,10 +154,10 @@ var GraphicsWebGLRenderer = function (renderer, src, drawingContext, parentMatri
case Commands.STROKE_PATH:
{
calcMatrix.multiply(currentMatrix, renderMatrix);
for (pathIndex = 0; pathIndex < path.length; pathIndex++)
{
calcMatrix.multiply(currentMatrix, renderMatrix);
(customRenderNodes.StrokePath || defaultRenderNodes.StrokePath).run(
currentContext,
submitterNode,
@ -161,7 +168,8 @@ var GraphicsWebGLRenderer = function (renderer, src, drawingContext, parentMatri
strokeTint.TL,
strokeTint.TR,
strokeTint.BL,
strokeTint.BR
strokeTint.BR,
pathDetailThreshold
);
}
break;

View file

@ -106,6 +106,7 @@ var WebGLRenderer = new Class({
backgroundColor: gameConfig.backgroundColor,
contextCreation: contextCreationConfig,
roundPixels: gameConfig.roundPixels,
pathDetailThreshold: gameConfig.pathDetailThreshold,
maxTextures: gameConfig.maxTextures,
maxTextureSize: gameConfig.maxTextureSize,
batchSize: gameConfig.batchSize,

View file

@ -45,11 +45,14 @@ var FillPath = new Class({
* @param {number} tintTL - The top-left tint color.
* @param {number} tintTR - The top-right tint color.
* @param {number} tintBL - The bottom-left tint color.
* @param {number} detail - The level of detail to use when filling the path. Points which are only this far apart in screen space are combined. It is ignored if the entire path is equal to or shorter than this distance.
*/
run: function (drawingContext, currentMatrix, submitterNode, path, tintTL, tintTR, tintBL)
run: function (drawingContext, currentMatrix, submitterNode, path, tintTL, tintTR, tintBL, detail)
{
this.onRunBegin(drawingContext);
if (detail === undefined) { detail = 0; }
var length = path.length;
var index, pathIndex, point, polygonIndexArray, x, y;
@ -57,32 +60,81 @@ var FillPath = new Class({
var verticesIndex = 0;
var indexedTrianglesIndex = 0;
var polygonCache = Array(length * 2);
var vertices = Array(length * 5);
var polygonCache = [];
var vertices = [];
for (pathIndex = 0; pathIndex < length; pathIndex++)
{
point = path[pathIndex];
// Transform the point.
x = currentMatrix.getX(point.x, point.y);
y = currentMatrix.getY(point.x, point.y);
if (
pathIndex > 0 &&
pathIndex < length - 1 &&
Math.abs(x - polygonCache[polygonCacheIndex - 2]) <= detail &&
Math.abs(y - polygonCache[polygonCacheIndex - 1]) <= detail
)
{
// Skip this point if it's too close to the previous point
// and is not the first or last point in the path.
continue;
}
polygonCache[polygonCacheIndex++] = x;
polygonCache[polygonCacheIndex++] = y;
}
polygonIndexArray = Earcut(polygonCache);
if (tintTL === tintTR && tintTL === tintBL)
{
// If the tint colors are all the same,
// then we can share vertices between the triangles.
for (pathIndex = 0; pathIndex < length; pathIndex++)
var polygonCacheLength = polygonCache.length;
for (index = 0; index < polygonCacheLength; index += 2)
{
point = path[pathIndex];
// Transform the point.
x = currentMatrix.getX(point.x, point.y);
y = currentMatrix.getY(point.x, point.y);
polygonCache[polygonCacheIndex++] = x;
polygonCache[polygonCacheIndex++] = y;
vertices[verticesIndex++] = x;
vertices[verticesIndex++] = y;
vertices[verticesIndex++] = polygonCache[index];
vertices[verticesIndex++] = polygonCache[index + 1];
vertices[verticesIndex++] = tintTL;
vertices[verticesIndex++] = -1;
vertices[verticesIndex++] = -1;
}
polygonIndexArray = Earcut(polygonCache);
length = polygonIndexArray.length;
// for (pathIndex = 0; pathIndex < length; pathIndex++)
// {
// point = path[pathIndex];
// // Transform the point.
// x = currentMatrix.getX(point.x, point.y);
// y = currentMatrix.getY(point.x, point.y);
// if (
// pathIndex > 0 &&
// pathIndex < length - 1 &&
// Math.abs(x - polygonCache[polygonCacheIndex - 2]) <= detail &&
// Math.abs(y - polygonCache[polygonCacheIndex - 1]) <= detail
// )
// {
// // Skip this point if it's too close to the previous point
// // and is not the first or last point in the path.
// continue;
// }
// polygonCache[polygonCacheIndex++] = x;
// polygonCache[polygonCacheIndex++] = y;
// vertices[verticesIndex++] = x;
// vertices[verticesIndex++] = y;
// vertices[verticesIndex++] = tintTL;
// vertices[verticesIndex++] = -1;
// vertices[verticesIndex++] = -1;
// }
// polygonIndexArray = Earcut(polygonCache);
submitterNode.batch(drawingContext, polygonIndexArray, vertices);
}
@ -90,24 +142,38 @@ var FillPath = new Class({
{
// If the tint colors are different,
// then we need to create a new vertex for each triangle.
for (pathIndex = 0; pathIndex < length; pathIndex++)
{
point = path[pathIndex];
// Transform the point.
x = currentMatrix.getX(point.x, point.y);
y = currentMatrix.getY(point.x, point.y);
polygonCache[polygonCacheIndex++] = x;
polygonCache[polygonCacheIndex++] = y;
}
// for (pathIndex = 0; pathIndex < length; pathIndex++)
// {
// point = path[pathIndex];
polygonIndexArray = Earcut(polygonCache);
length = polygonIndexArray.length;
// // Transform the point.
// x = currentMatrix.getX(point.x, point.y);
// y = currentMatrix.getY(point.x, point.y);
var indexedTriangles = Array(length);
// if (
// pathIndex > 0 &&
// pathIndex < length - 1 &&
// Math.abs(x - polygonCache[polygonCacheIndex - 2]) <= detail &&
// Math.abs(y - polygonCache[polygonCacheIndex - 1]) <= detail
// )
// {
// // Skip this point if it's too close to the previous point
// // and is not the first or last point in the path.
// continue;
// }
for (index = 0; index < length; index += 3)
// polygonCache[polygonCacheIndex++] = x;
// polygonCache[polygonCacheIndex++] = y;
// }
// polygonIndexArray = Earcut(polygonCache);
var indexLength = polygonIndexArray.length;
var indexedTriangles = Array(indexLength);
for (index = 0; index < indexLength; index += 3)
{
// Vertex A
var p = polygonIndexArray[index] * 2;

View file

@ -51,8 +51,9 @@ var StrokePath = new Class({
* @param {number} tintTR - The top-right tint color.
* @param {number} tintBL - The bottom-left tint color.
* @param {number} tintBR - The bottom-right tint color.
* @param {number} detail - The level of detail to use when rendering the stroke. Points which are only this far apart in screen space are combined. It is ignored if the entire path is equal to or shorter than this distance.
*/
run: function (drawingContext, submitterNode, path, lineWidth, open, currentMatrix, tintTL, tintTR, tintBL, tintBR)
run: function (drawingContext, submitterNode, path, lineWidth, open, currentMatrix, tintTL, tintTR, tintBL, tintBR, detail)
{
this.onRunBegin(drawingContext);
@ -62,37 +63,62 @@ var StrokePath = new Class({
var point, nextPoint;
// Determine size of index array.
var indexCount = pathLength * 6;
// Determine connectivity of index array.
var connect = false;
var connectLoop = false;
if (lineWidth > 2 && pathLength > 1)
{
connect = true;
// Lines will be connected by a secondary quad.
indexCount *= 2;
if (open)
{
// The last line will not be connected to the first line.
indexCount -= 6;
}
else
connect = true;
if (!open)
{
// The last line will be connected to the first line.
connectLoop = true;
}
}
var indices = Array(indexCount);
var indices = [];
var indexOffset = 0;
var vertices = Array(pathLength * 4 * 5);
var vertices = [];
var vertexOffset = 0;
var vertexCount;
for (var i = 0; i < pathLength; i++)
var dx, dy, tdx, tdy;
var detailSquared = detail * detail;
var first, last, iterate;
for (var i = 0; i < pathLength; i += iterate)
{
first = i === 0;
last = i === pathLength - 1;
iterate = 1;
point = path[i];
nextPoint = path[i + 1];
nextPoint = path[i + iterate];
if (detailSquared && !last)
{
dx = nextPoint.x - point.x;
dy = nextPoint.y - point.y;
tdx = currentMatrix.getX(dx, dy) - currentMatrix.tx;
tdy = currentMatrix.getY(dx, dy) - currentMatrix.ty;
while (
i + iterate < pathLength - 1 &&
tdx * tdx + tdy * tdy <= detailSquared
)
{
// Skip the next point if it's too close to the current point.
iterate++;
nextPoint = path[i + iterate];
dx = nextPoint.x - point.x;
dy = nextPoint.y - point.y;
tdx = currentMatrix.getX(dx, dy) - currentMatrix.tx;
tdy = currentMatrix.getY(dx, dy) - currentMatrix.ty;
}
}
drawLineNode.run(
drawingContext,
@ -131,16 +157,18 @@ var StrokePath = new Class({
vertices[vertexOffset++] = -1;
vertices[vertexOffset++] = -1;
vertexCount = vertexOffset / 5;
// Draw two triangles.
// The vertices are in the order: TL, BL, BR, TR
indices[indexOffset++] = i * 4;
indices[indexOffset++] = i * 4 + 1;
indices[indexOffset++] = i * 4 + 2;
indices[indexOffset++] = i * 4 + 2;
indices[indexOffset++] = i * 4 + 3;
indices[indexOffset++] = i * 4;
indices[indexOffset++] = vertexCount - 4;
indices[indexOffset++] = vertexCount - 3;
indices[indexOffset++] = vertexCount - 2;
indices[indexOffset++] = vertexCount - 2;
indices[indexOffset++] = vertexCount - 1;
indices[indexOffset++] = vertexCount - 4;
if (connect && i !== 0)
if (connect && !first)
{
// Draw a quad connecting to the previous line segment.
// The vertices are in the order:
@ -148,14 +176,14 @@ var StrokePath = new Class({
// - BL
// - Previous BR
// - Previous TR
indices[indexOffset++] = i * 4;
indices[indexOffset++] = i * 4 + 1;
indices[indexOffset++] = i * 4 - 2;
indices[indexOffset++] = i * 4 - 2;
indices[indexOffset++] = i * 4 - 1;
indices[indexOffset++] = i * 4;
indices[indexOffset++] = vertexCount - 4;
indices[indexOffset++] = vertexCount - 3;
indices[indexOffset++] = vertexCount - 6;
indices[indexOffset++] = vertexCount - 6;
indices[indexOffset++] = vertexCount - 5;
indices[indexOffset++] = vertexCount - 4;
if (connectLoop && i === pathLength - 1)
if (connectLoop && last)
{
// Connect the last line segment to the first.
// The vertices are in the order:
@ -163,12 +191,12 @@ var StrokePath = new Class({
// - TR
// - First TL
// - First BL
indices[indexOffset++] = i * 4 + 2;
indices[indexOffset++] = i * 4 + 3;
indices[indexOffset++] = vertexCount - 2;
indices[indexOffset++] = vertexCount - 1;
indices[indexOffset++] = 0;
indices[indexOffset++] = 0;
indices[indexOffset++] = 1;
indices[indexOffset++] = i * 4 + 2;
indices[indexOffset++] = vertexCount - 2;
}
}
}