diff --git a/examples/src/examples/gizmos/transform-rotate.example.mjs b/examples/src/examples/gizmos/transform-rotate.example.mjs
index 68ed8e189ec..4100ef4205d 100644
--- a/examples/src/examples/gizmos/transform-rotate.example.mjs
+++ b/examples/src/examples/gizmos/transform-rotate.example.mjs
@@ -1,7 +1,9 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath } from 'examples/utils';
+import { deviceType, fileImport, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
+const { GridRenderer } = await fileImport(`${rootPath}/static/scripts/esm/grid-renderer.mjs`);
+
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -82,6 +84,16 @@ data.set('camera', {
fov: camera.camera.fov
});
+// create grid
+const gridEntity = new pc.Entity('grid');
+gridEntity.addComponent('script');
+gridEntity.script.create(GridRenderer, {
+ attributes: {
+ halfExtents: new pc.Vec2(2, 2)
+ }
+});
+app.root.addChild(gridEntity);
+
// create light entity
const light = new pc.Entity('light');
light.addComponent('light');
@@ -146,24 +158,6 @@ const resize = () => {
window.addEventListener('resize', resize);
resize();
-// grid lines
-const createGridLines = (size = 1) => {
- const lines = [];
- for (let i = -size; i < size + 1; i++) {
- lines.push(
- new pc.Vec3(-size, 0, i),
- new pc.Vec3(size, 0, i),
- new pc.Vec3(i, 0, -size),
- new pc.Vec3(i, 0, size)
- );
- }
- return lines;
-};
-
-const lines = createGridLines(2);
-const gridCol = new pc.Color(1, 1, 1, 0.5);
-app.on('update', () => app.drawLines(lines, gridCol));
-
app.on('destroy', () => {
window.removeEventListener('resize', resize);
});
diff --git a/examples/src/examples/gizmos/transform-scale.example.mjs b/examples/src/examples/gizmos/transform-scale.example.mjs
index 7b2fae32e3a..57566df2b95 100644
--- a/examples/src/examples/gizmos/transform-scale.example.mjs
+++ b/examples/src/examples/gizmos/transform-scale.example.mjs
@@ -1,7 +1,9 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath } from 'examples/utils';
+import { deviceType, fileImport, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';
+const { GridRenderer } = await fileImport(`${rootPath}/static/scripts/esm/grid-renderer.mjs`);
+
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -82,6 +84,16 @@ data.set('camera', {
fov: camera.camera.fov
});
+// create grid
+const gridEntity = new pc.Entity('grid');
+gridEntity.addComponent('script');
+gridEntity.script.create(GridRenderer, {
+ attributes: {
+ halfExtents: new pc.Vec2(2, 2)
+ }
+});
+app.root.addChild(gridEntity);
+
// create light entity
const light = new pc.Entity('light');
light.addComponent('light');
@@ -150,24 +162,6 @@ const resize = () => {
window.addEventListener('resize', resize);
resize();
-// grid lines
-const createGridLines = (size = 1) => {
- const lines = [];
- for (let i = -size; i < size + 1; i++) {
- lines.push(
- new pc.Vec3(-size, 0, i),
- new pc.Vec3(size, 0, i),
- new pc.Vec3(i, 0, -size),
- new pc.Vec3(i, 0, size)
- );
- }
- return lines;
-};
-
-const lines = createGridLines(2);
-const gridCol = new pc.Color(1, 1, 1, 0.5);
-app.on('update', () => app.drawLines(lines, gridCol));
-
app.on('destroy', () => {
window.removeEventListener('resize', resize);
});
diff --git a/examples/src/examples/gizmos/transform-translate.example.mjs b/examples/src/examples/gizmos/transform-translate.example.mjs
index bf5372a42cd..60c2eaa79f7 100644
--- a/examples/src/examples/gizmos/transform-translate.example.mjs
+++ b/examples/src/examples/gizmos/transform-translate.example.mjs
@@ -1,7 +1,9 @@
import { data } from 'examples/observer';
-import { deviceType, rootPath } from 'examples/utils';
+import { deviceType, rootPath, fileImport } from 'examples/utils';
import * as pc from 'playcanvas';
+const { GridRenderer } = await fileImport(`${rootPath}/static/scripts/esm/grid-renderer.mjs`);
+
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
@@ -82,6 +84,16 @@ data.set('camera', {
fov: camera.camera.fov
});
+// create grid
+const gridEntity = new pc.Entity('grid');
+gridEntity.addComponent('script');
+gridEntity.script.create(GridRenderer, {
+ attributes: {
+ halfExtents: new pc.Vec2(2, 2)
+ }
+});
+app.root.addChild(gridEntity);
+
// create light entity
const light = new pc.Entity('light');
light.addComponent('light');
@@ -151,24 +163,6 @@ const resize = () => {
window.addEventListener('resize', resize);
resize();
-// grid lines
-const createGridLines = (size = 1) => {
- const lines = [];
- for (let i = -size; i < size + 1; i++) {
- lines.push(
- new pc.Vec3(-size, 0, i),
- new pc.Vec3(size, 0, i),
- new pc.Vec3(i, 0, -size),
- new pc.Vec3(i, 0, size)
- );
- }
- return lines;
-};
-
-const lines = createGridLines(2);
-const gridCol = new pc.Color(1, 1, 1, 0.5);
-app.on('update', () => app.drawLines(lines, gridCol));
-
app.on('destroy', () => {
window.removeEventListener('resize', resize);
});
diff --git a/examples/src/examples/misc/editor.controls.mjs b/examples/src/examples/misc/editor.controls.mjs
index 64ed57e3689..b497e07b3f4 100644
--- a/examples/src/examples/misc/editor.controls.mjs
+++ b/examples/src/examples/misc/editor.controls.mjs
@@ -5,7 +5,7 @@ import * as pc from 'playcanvas';
* @returns {JSX.Element} The returned JSX Element.
*/
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
- const { BindingTwoWay, LabelGroup, Panel, SliderInput, SelectInput } = ReactPCUI;
+ const { BindingTwoWay, LabelGroup, Panel, SliderInput, SelectInput, ColorPicker } = ReactPCUI;
const { useState } = React;
const [type, setType] = useState('translate');
@@ -87,6 +87,50 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
})
)
),
+ jsx(
+ Panel,
+ { headerText: 'Grid' },
+ jsx(
+ LabelGroup,
+ { text: 'Resolution' },
+ jsx(SelectInput, {
+ options: [
+ { v: 3, t: 'High' },
+ { v: 2, t: 'Medium' },
+ { v: 1, t: 'Low' }
+ ],
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'grid.resolution' }
+ })
+ ),
+ jsx(
+ LabelGroup,
+ { text: 'Size' },
+ jsx(SliderInput, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'grid.size' },
+ min: 0,
+ max: 10,
+ precision: 0
+ })
+ ),
+ jsx(
+ LabelGroup,
+ { text: 'Color X' },
+ jsx(ColorPicker, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'grid.colorX' }
+ })
+ ),
+ jsx(
+ LabelGroup,
+ { text: 'Color Z' },
+ jsx(ColorPicker, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'grid.colorZ' }
+ })
+ )
+ ),
jsx(
Panel,
{ headerText: 'Camera' },
diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs
index f50f1670de3..a94cdb1cc8d 100644
--- a/examples/src/examples/misc/editor.example.mjs
+++ b/examples/src/examples/misc/editor.example.mjs
@@ -1,14 +1,15 @@
// @config DESCRIPTION
Translate (1), Rotate (2), Scale (3)
World/Local (X)
Perspective (P), Orthographic (O)
import { data } from 'examples/observer';
-import { deviceType, rootPath, localImport } from 'examples/utils';
+import { deviceType, rootPath, localImport, fileImport } from 'examples/utils';
import * as pc from 'playcanvas';
+const { GridRenderer } = await fileImport(`${rootPath}/static/scripts/esm/grid-renderer.mjs`);
+
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();
// class for handling gizmo
const { GizmoHandler } = await localImport('gizmo-handler.mjs');
-const { Grid } = await localImport('grid.mjs');
const { Selector } = await localImport('selector.mjs');
const gfxOptions = {
@@ -127,6 +128,22 @@ camera.setPosition(1, 1, 1);
app.root.addChild(camera);
orbitCamera.distance = 5 * camera.camera?.aspectRatio;
+// create grid
+const gridEntity = new pc.Entity('grid');
+gridEntity.addComponent('script');
+const grid = /** @type {GridRenderer} */ (gridEntity.script.create(GridRenderer, {
+ attributes: {
+ halfExtents: new pc.Vec2(4, 4)
+ }
+}));
+app.root.addChild(gridEntity);
+data.set('grid', {
+ resolution: grid.resolution + 1,
+ size: grid.halfExtents.x,
+ colorX: Object.values(grid.colorX),
+ colorZ: Object.values(grid.colorZ)
+});
+
// create light entity
const light = new pc.Entity('light');
light.addComponent('light', {
@@ -204,6 +221,8 @@ window.addEventListener('keyup', keyup);
window.addEventListener('keypress', keypress);
// gizmo and camera set handler
+const tmpVa = new pc.Vec2();
+const tmpC1 = new pc.Color();
data.on('*:set', (/** @type {string} */ path, /** @type {any} */ value) => {
const [category, key] = path.split('.');
switch (category) {
@@ -230,6 +249,23 @@ data.on('*:set', (/** @type {string} */ path, /** @type {any} */ value) => {
gizmoHandler.gizmo[key] = value;
break;
}
+ case 'grid': {
+ switch (key) {
+ case 'resolution':
+ grid.resolution = value - 1;
+ break;
+ case 'size':
+ grid.halfExtents = tmpVa.set(value, value);
+ break;
+ case 'colorX':
+ grid.colorX = tmpC1.set(value[0], value[1], value[2]);
+ break;
+ case 'colorZ':
+ grid.colorZ = tmpC1.set(value[0], value[1], value[2]);
+ break;
+ }
+ break;
+ }
}
});
@@ -246,13 +282,6 @@ selector.on('deselect', () => {
gizmoHandler.clear();
});
-// grid
-const grid = new Grid();
-
-app.on('update', (/** @type {number} */ dt) => {
- grid.draw(app);
-});
-
app.on('destroy', () => {
gizmoHandler.destroy();
selector.destroy();
diff --git a/examples/src/examples/misc/editor.grid.mjs b/examples/src/examples/misc/editor.grid.mjs
deleted file mode 100644
index 101b4299769..00000000000
--- a/examples/src/examples/misc/editor.grid.mjs
+++ /dev/null
@@ -1,77 +0,0 @@
-import * as pc from 'playcanvas';
-
-class Grid {
- /**
- * @type {pc.Vec3[]}
- * @private
- */
- _lines = [];
-
- /**
- * @type {pc.Color}
- * @private
- */
- _color = new pc.Color(1, 1, 1, 0.5);
-
- /**
- * @type {pc.Vec2}
- * @private
- */
- _halfExtents = new pc.Vec2(4, 4);
-
- constructor() {
- this._setLines();
- }
-
- set halfExtents(value) {
- this._halfExtents.copy(value);
- this._setLines();
- }
-
- get halfExtents() {
- return this._halfExtents;
- }
-
- set color(value) {
- this._color.copy(value);
- }
-
- get color() {
- return this._color;
- }
-
- /**
- * @private
- */
- _setLines() {
- this._lines = [
- new pc.Vec3(-this._halfExtents.x, 0, 0),
- new pc.Vec3(this._halfExtents.x, 0, 0),
- new pc.Vec3(0, 0, -this._halfExtents.y),
- new pc.Vec3(0, 0, this._halfExtents.y)
- ];
- for (let i = -this._halfExtents.x; i <= this._halfExtents.x; i++) {
- if (i === 0) {
- continue;
- }
- this._lines.push(new pc.Vec3(i, 0, -this._halfExtents.y));
- this._lines.push(new pc.Vec3(i, 0, this._halfExtents.y));
- }
- for (let i = -this._halfExtents.y; i <= this._halfExtents.y; i++) {
- if (i === 0) {
- continue;
- }
- this._lines.push(new pc.Vec3(-this._halfExtents.x, 0, i));
- this._lines.push(new pc.Vec3(this._halfExtents.x, 0, i));
- }
- }
-
- /**
- * @param {pc.AppBase} app - The app.
- */
- draw(app) {
- app.drawLines(this._lines, this._color);
- }
-}
-
-export { Grid };
diff --git a/examples/thumbnails/gizmos_transform-rotate_large.webp b/examples/thumbnails/gizmos_transform-rotate_large.webp
index 49933c54bbb..2f9ccefbe9a 100644
Binary files a/examples/thumbnails/gizmos_transform-rotate_large.webp and b/examples/thumbnails/gizmos_transform-rotate_large.webp differ
diff --git a/examples/thumbnails/gizmos_transform-rotate_small.webp b/examples/thumbnails/gizmos_transform-rotate_small.webp
index cde8ac63e73..e981618cf1a 100644
Binary files a/examples/thumbnails/gizmos_transform-rotate_small.webp and b/examples/thumbnails/gizmos_transform-rotate_small.webp differ
diff --git a/examples/thumbnails/gizmos_transform-scale_large.webp b/examples/thumbnails/gizmos_transform-scale_large.webp
index 4384e4c2fc4..97e475d5601 100644
Binary files a/examples/thumbnails/gizmos_transform-scale_large.webp and b/examples/thumbnails/gizmos_transform-scale_large.webp differ
diff --git a/examples/thumbnails/gizmos_transform-scale_small.webp b/examples/thumbnails/gizmos_transform-scale_small.webp
index 774b181d4b4..8accf844288 100644
Binary files a/examples/thumbnails/gizmos_transform-scale_small.webp and b/examples/thumbnails/gizmos_transform-scale_small.webp differ
diff --git a/examples/thumbnails/gizmos_transform-translate_large.webp b/examples/thumbnails/gizmos_transform-translate_large.webp
index cfe4cda1bf0..871f19048de 100644
Binary files a/examples/thumbnails/gizmos_transform-translate_large.webp and b/examples/thumbnails/gizmos_transform-translate_large.webp differ
diff --git a/examples/thumbnails/gizmos_transform-translate_small.webp b/examples/thumbnails/gizmos_transform-translate_small.webp
index 80068a6d49d..c265972961e 100644
Binary files a/examples/thumbnails/gizmos_transform-translate_small.webp and b/examples/thumbnails/gizmos_transform-translate_small.webp differ
diff --git a/examples/thumbnails/misc_editor_large.webp b/examples/thumbnails/misc_editor_large.webp
index 1bd19cab794..a0ca2ab1097 100644
Binary files a/examples/thumbnails/misc_editor_large.webp and b/examples/thumbnails/misc_editor_large.webp differ
diff --git a/examples/thumbnails/misc_editor_small.webp b/examples/thumbnails/misc_editor_small.webp
index 465d9bc7287..6adfb5dffdd 100644
Binary files a/examples/thumbnails/misc_editor_small.webp and b/examples/thumbnails/misc_editor_small.webp differ
diff --git a/scripts/esm/grid-renderer.mjs b/scripts/esm/grid-renderer.mjs
new file mode 100644
index 00000000000..24d2e554d5a
--- /dev/null
+++ b/scripts/esm/grid-renderer.mjs
@@ -0,0 +1,488 @@
+import {
+ BLENDMODE_ONE,
+ BLENDMODE_ONE_MINUS_SRC_ALPHA,
+ BLENDMODE_SRC_ALPHA,
+ BLENDEQUATION_ADD,
+ CULLFACE_NONE,
+ PROJECTION_PERSPECTIVE,
+ SEMANTIC_POSITION,
+ BlendState,
+ DepthState,
+ QuadRender,
+ Layer,
+ createShaderFromCode,
+ Script,
+ Color,
+ Mat4,
+ Vec2,
+ Vec3
+// eslint-disable-next-line import/no-unresolved
+} from 'playcanvas';
+
+/** @import { AppBase, CameraComponent, GraphicsDevice } from 'playcanvas' */
+
+// constants
+const LAYER_NAME = 'Grid';
+
+// temporary variables
+const tmpV1 = new Vec3();
+const tmpM1 = new Mat4();
+
+const vertexShader = /* glsl*/ `
+ uniform vec3 near_origin;
+ uniform vec3 near_x;
+ uniform vec3 near_y;
+
+ uniform vec3 far_origin;
+ uniform vec3 far_x;
+ uniform vec3 far_y;
+
+ attribute vec2 vertex_position;
+
+ varying vec3 worldFar;
+ varying vec3 worldNear;
+
+ void main(void) {
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
+
+ vec2 p = vertex_position * 0.5 + 0.5;
+ worldNear = near_origin + near_x * p.x + near_y * p.y;
+ worldFar = far_origin + far_x * p.x + far_y * p.y;
+ }
+`;
+
+const fragmentShader = /* glsl*/ `
+ uniform vec3 view_position;
+ uniform mat4 matrix_viewProjection;
+ uniform sampler2D blueNoiseTex32;
+
+ uniform int resolution;
+
+ uniform vec2 half_extents;
+
+ uniform mat4 inv_transform;
+
+ uniform vec3 color_x;
+ uniform vec3 color_z;
+
+ uniform bool depthMode;
+
+ varying vec3 worldNear;
+ varying vec3 worldFar;
+
+ bool intersectPlane(inout float t, vec3 pos, vec3 dir, vec4 plane) {
+ float d = dot(dir, plane.xyz);
+ if (abs(d) < 1e-06) {
+ return false;
+ }
+
+ float n = -(dot(pos, plane.xyz) + plane.w) / d;
+ if (n < 0.0) {
+ return false;
+ }
+
+ t = n;
+
+ return true;
+ }
+
+ vec3 transformPoint(mat4 m, vec3 p) {
+ return (m * vec4(p, 1.0)).xyz;
+ }
+
+ vec3 transformDirection(mat4 m, vec3 p) {
+ return (m * vec4(p, 0.0)).xyz;
+ }
+
+ // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c
+ float pristineGrid(in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) {
+ vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y)));
+ bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5);
+ vec2 targetWidth = vec2(
+ invertLine.x ? 1.0 - lineWidth.x : lineWidth.x,
+ invertLine.y ? 1.0 - lineWidth.y : lineWidth.y
+ );
+ vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
+ vec2 lineAA = uvDeriv * 1.5;
+ vec2 gridUV = abs(fract(uv) * 2.0 - 1.0);
+ gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x;
+ gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y;
+ vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
+
+ grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
+ grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0));
+ grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x;
+ grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y;
+
+ return mix(grid2.x, 1.0, grid2.y);
+ }
+
+ float calcDepth(vec3 p) {
+ vec4 v = matrix_viewProjection * vec4(p, 1.0);
+ #ifdef WEBGPU
+ return (v.z / v.w);
+ #else
+ return (v.z / v.w) * 0.5 + 0.5;
+ #endif
+ }
+
+ bool writeDepth(float alpha) {
+ if (!depthMode) return true;
+ vec2 uv = fract(gl_FragCoord.xy / 32.0);
+ float noise = texture2DLodEXT(blueNoiseTex32, uv, 0.0).y;
+ return alpha > noise;
+ }
+
+ void main(void) {
+ vec3 p = transformPoint(inv_transform, worldNear);
+ vec3 v = transformDirection(inv_transform, normalize(worldFar - worldNear));
+
+ // intersect ray with the world xz plane
+ float t;
+ if (!intersectPlane(t, p, v, vec4(0, 1, 0, 0))) {
+ discard;
+ }
+
+ // calculate grid intersection
+ vec3 pos = p + v * t;
+ vec2 ddx = dFdx(pos.xz);
+ vec2 ddy = dFdy(pos.xz);
+
+ float epsilon = 1.0 / 255.0;
+
+ // discard if outside size
+ if (abs(pos.x) > half_extents.x || abs(pos.z) > half_extents.y) {
+ discard;
+ }
+
+ // calculate fade
+ float fade = 1.0 - smoothstep(400.0, 1000.0, length(pos - view_position));
+ if (fade < epsilon) {
+ discard;
+ }
+
+ vec3 levelPos;
+ float levelSize;
+ float levelAlpha;
+
+ // 10m grid with colored main axes
+ levelPos = pos * 0.1;
+ levelSize = 2.0 / 1000.0;
+ levelAlpha = pristineGrid(levelPos.xz, ddx * 0.1, ddy * 0.1, vec2(levelSize)) * fade;
+ if (levelAlpha > epsilon) {
+ vec3 color;
+ vec2 loc = abs(levelPos.xz);
+ if (loc.x < levelSize) {
+ if (loc.y < levelSize) {
+ color = vec3(1.0);
+ } else {
+ color = color_z;
+ }
+ } else if (loc.y < levelSize) {
+ color = color_x;
+ } else {
+ color = vec3(0.9);
+ }
+ gl_FragColor = vec4(color, levelAlpha);
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(pos) : 1.0;
+ return;
+ }
+
+ // 1m grid
+ levelPos = pos;
+ levelSize = 1.0 / 100.0;
+ levelAlpha = pristineGrid(levelPos.xz, ddx, ddy, vec2(levelSize)) * fade;
+ if (levelAlpha > epsilon) {
+ if (resolution < 1) {
+ discard;
+ }
+ gl_FragColor = vec4(vec3(0.7), levelAlpha);
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(pos) : 1.0;
+ return;
+ }
+
+ // 0.1m grid
+ levelPos = pos * 10.0;
+ levelSize = 1.0 / 100.0;
+ levelAlpha = pristineGrid(levelPos.xz, ddx * 10.0, ddy * 10.0, vec2(levelSize)) * fade;
+ if (levelAlpha > epsilon) {
+ if (resolution < 2) {
+ discard;
+ }
+ gl_FragColor = vec4(vec3(0.7), levelAlpha);
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(pos) : 1.0;
+ return;
+ }
+
+ discard;
+ }
+`;
+
+class GridRenderer extends Script {
+ /**
+ * @type {number}
+ */
+ static RESOLUTION_LOW = 0;
+
+ /**
+ * @type {number}
+ */
+ static RESOLUTION_MEDIUM = 1;
+
+ /**
+ * @type {number}
+ */
+ static RESOLUTION_HIGH = 2;
+
+ /**
+ * @type {number}
+ */
+ _resolution = GridRenderer.RESOLUTION_HIGH;
+
+ /**
+ * @type {GraphicsDevice}
+ */
+ _device;
+
+ /**
+ * @type {QuadRender}
+ * @private
+ */
+ _quadRender;
+
+ /**
+ * @type {BlendState}
+ */
+ _blendState = new BlendState(
+ true,
+ BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA,
+ BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA
+ );
+
+ /**
+ * @type {Vec2}
+ */
+ _halfExtents = new Vec2(Infinity, Infinity);
+
+ /**
+ * @type {Color}
+ * @private
+ */
+ _colorX = new Color(1, 0.3, 0.3);
+
+ /**
+ * @type {Color}
+ * @private
+ */
+ _colorZ = new Color(0.3, 0.3, 1);
+
+ /**
+ * Creates a new layer for the grid.
+ *
+ * @param {AppBase} app - The app.
+ * @param {string} [layerName] - The layer name. Defaults to 'Grid'.
+ * @param {number} [layerIndex] - The layer index. Defaults to the end of the layer list.
+ * @returns {Layer} The new layer.
+ */
+ static createLayer(app, layerName = LAYER_NAME, layerIndex) {
+ const layer = new Layer({
+ name: layerName,
+ clearDepthBuffer: false
+ });
+ app.scene.layers.insert(layer, layerIndex ?? app.scene.layers.layerList.length);
+ return layer;
+ }
+
+ /**
+ * @param {object} args - The script arguments.
+ */
+ constructor(args) {
+ super(args);
+ const {
+ layer,
+ resolution,
+ halfExtents,
+ colorX,
+ colorZ
+ } = args.attributes;
+
+ this.resolution = resolution;
+ this.halfExtents = halfExtents;
+ this.colorX = colorX;
+ this.colorZ = colorZ;
+
+ this._device = this.app.graphicsDevice;
+
+ // create shader
+ const shader = createShaderFromCode(this._device, vertexShader, fragmentShader, 'grid', {
+ vertex_position: SEMANTIC_POSITION
+ });
+ this._quadRender = new QuadRender(shader);
+
+ const targetLayer = layer ?? GridRenderer.createLayer(this.app, undefined, 1);
+
+ const cameras = [];
+ this.app.scene.on('prerender:layer', (camera, layer, transparent) => {
+ if (!cameras.includes(camera)) {
+ cameras.push(camera);
+ }
+
+ if (layer !== targetLayer || transparent) {
+ return;
+ }
+
+ // get frustum corners in world space
+ const points = camera.camera.getFrustumCorners(-100);
+ const worldTransform = camera.entity.getWorldTransform();
+ for (let i = 0; i < points.length; i++) {
+ worldTransform.transformPoint(points[i], points[i]);
+ }
+
+ // near
+ if (camera.projection === PROJECTION_PERSPECTIVE) {
+ // perspective
+ this._set('near_origin', worldTransform.getTranslation());
+ this._set('near_x', Vec3.ZERO);
+ this._set('near_y', Vec3.ZERO);
+ } else {
+ // orthographic
+ this._set('near_origin', points[3]);
+ this._set('near_x', tmpV1.sub2(points[0], points[3]));
+ this._set('near_y', tmpV1.sub2(points[2], points[3]));
+ }
+
+ // far
+ this._set('far_origin', points[7]);
+ this._set('far_x', tmpV1.sub2(points[4], points[7]));
+ this._set('far_y', tmpV1.sub2(points[6], points[7]));
+
+ // resolution
+ this._set('resolution', this._resolution);
+
+ // size
+ this._set('half_extents', this._halfExtents);
+
+ // transform
+ this._set('inv_transform', tmpM1.copy(this.entity.getWorldTransform()).invert());
+
+ // colors
+ this._set('color_x', this._colorX);
+ this._set('color_z', this._colorZ);
+
+ this._device.setCullMode(CULLFACE_NONE);
+ this._device.setStencilState(null, null);
+ this._device.setDepthState(DepthState.DEFAULT);
+
+ // write color
+ this._set('depthMode', false);
+ this._device.setBlendState(this._blendState);
+ this._quadRender.render();
+
+ // write depth
+ this._set('depthMode', true);
+ this._device.setBlendState(BlendState.NOWRITE);
+ this._quadRender.render();
+ });
+
+ this.app.on('update', () => {
+ for (const camera of cameras) {
+ if (!camera.layers.includes(targetLayer.id)) {
+ camera.layers = camera.layers.concat(targetLayer.id);
+ }
+ }
+ });
+ }
+
+ /**
+ * Set the value of a uniform in the shader.
+ *
+ * @param {string} name - The name of the uniform.
+ * @param {Color|Vec3|number} value - The value to set.
+ * @private
+ */
+ _set(name, value) {
+ if (value instanceof Color) {
+ this._device.scope.resolve(name).setValue([value.r, value.g, value.b]);
+ }
+
+ if (value instanceof Mat4) {
+ this._device.scope.resolve(name).setValue(value.data);
+ }
+
+ if (value instanceof Vec3) {
+ this._device.scope.resolve(name).setValue([value.x, value.y, value.z]);
+ }
+
+ if (value instanceof Vec2) {
+ this._device.scope.resolve(name).setValue([value.x, value.y]);
+ }
+
+ if (typeof value === 'number') {
+ this._device.scope.resolve(name).setValue(value);
+ }
+
+ }
+
+ /**
+ * @attribute
+ * @type {GridRenderer.RESOLUTION_HIGH | GridRenderer.RESOLUTION_MEDIUM | GridRenderer.RESOLUTION_LOW}
+ */
+ set resolution(value) {
+ this._resolution = value ?? GridRenderer.RESOLUTION_HIGH;
+ }
+
+ get resolution() {
+ return this._resolution;
+ }
+
+ /**
+ * @attribute
+ * @type {Vec2}
+ */
+ set halfExtents(value) {
+ if (!(value instanceof Vec2)) {
+ return;
+ }
+ this._halfExtents.set(value.x || Infinity, value.y || Infinity);
+ }
+
+ get halfExtents() {
+ return this._halfExtents;
+ }
+
+ /**
+ * @attribute
+ * @type {Color}
+ */
+ set colorX(value) {
+ if (!(value instanceof Color)) {
+ return;
+ }
+ this._colorX.copy(value);
+ }
+
+ get colorX() {
+ return this._colorX;
+ }
+
+ /**
+ * @attribute
+ * @type {Color}
+ */
+ set colorZ(value) {
+ if (!(value instanceof Color)) {
+ return;
+ }
+ this._colorZ.copy(value);
+ }
+
+ get colorZ() {
+ return this._colorZ;
+ }
+
+ destroy() {
+ this._quadRender.destroy();
+ }
+}
+
+export { GridRenderer };