diff --git a/examples/src/examples/gizmos/bounding-box.example.mjs b/examples/src/examples/gizmos/bounding-box.example.mjs
index a33371b6fe2..1345d4d6af3 100644
--- a/examples/src/examples/gizmos/bounding-box.example.mjs
+++ b/examples/src/examples/gizmos/bounding-box.example.mjs
@@ -130,7 +130,6 @@ data.set('gizmo', {
// Create grid
const gridEntity = new Entity('grid');
-gridEntity.setLocalScale(4, 1, 4);
app.root.addChild(gridEntity);
gridEntity.addComponent('script');
gridEntity.script.create(Grid);
diff --git a/examples/src/examples/gizmos/transform-rotate.example.mjs b/examples/src/examples/gizmos/transform-rotate.example.mjs
index 0ce05c66261..c5f4288381c 100644
--- a/examples/src/examples/gizmos/transform-rotate.example.mjs
+++ b/examples/src/examples/gizmos/transform-rotate.example.mjs
@@ -162,7 +162,6 @@ data.set('gizmo', {
// Create grid
const gridEntity = new Entity('grid');
-gridEntity.setLocalScale(4, 1, 4);
app.root.addChild(gridEntity);
gridEntity.addComponent('script');
gridEntity.script.create(Grid);
diff --git a/examples/src/examples/gizmos/transform-scale.example.mjs b/examples/src/examples/gizmos/transform-scale.example.mjs
index 0d6d9f36e99..735c73b5040 100644
--- a/examples/src/examples/gizmos/transform-scale.example.mjs
+++ b/examples/src/examples/gizmos/transform-scale.example.mjs
@@ -164,7 +164,6 @@ data.set('gizmo', {
// Create grid
const gridEntity = new Entity('grid');
-gridEntity.setLocalScale(4, 1, 4);
app.root.addChild(gridEntity);
gridEntity.addComponent('script');
gridEntity.script.create(Grid);
diff --git a/examples/src/examples/gizmos/transform-translate.example.mjs b/examples/src/examples/gizmos/transform-translate.example.mjs
index d51096365cc..bc256c53f9e 100644
--- a/examples/src/examples/gizmos/transform-translate.example.mjs
+++ b/examples/src/examples/gizmos/transform-translate.example.mjs
@@ -164,7 +164,6 @@ data.set('gizmo', {
// Create grid
const gridEntity = new Entity('grid');
-gridEntity.setLocalScale(4, 1, 4);
app.root.addChild(gridEntity);
gridEntity.addComponent('script');
gridEntity.script.create(Grid);
diff --git a/examples/src/examples/graphics/grid.controls.jsx b/examples/src/examples/graphics/grid.controls.jsx
new file mode 100644
index 00000000000..409dbe47b0b
--- /dev/null
+++ b/examples/src/examples/graphics/grid.controls.jsx
@@ -0,0 +1,163 @@
+import {
+ BindingTwoWay,
+ BooleanInput,
+ ColorPicker,
+ LabelGroup,
+ Panel,
+ SelectInput,
+ SliderInput,
+ VectorInput
+} from '@playcanvas/pcui/react';
+import { useEffect, useState } from 'react';
+
+import { PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE } from 'playcanvas';
+
+/**
+ * @import { Observer } from '@playcanvas/observer'
+ * @import { ReactElement } from 'react'
+ */
+
+/**
+ * @param {{ observer: Observer }} props - The control panel props.
+ * @returns {ReactElement} The control panel.
+ */
+export function Controls({ observer }) {
+ // size only when not infinite
+ const [infinite, setInfinite] = useState(observer.get('grid.infinite') ?? true);
+ useEffect(() => {
+ const event = observer.on('grid.infinite:set', setInfinite);
+ return () => event?.unbind();
+ }, [observer]);
+
+ // axis thickness only when axes show
+ const [showAxes, setShowAxes] = useState(observer.get('grid.showAxes') ?? true);
+ useEffect(() => {
+ const event = observer.on('grid.showAxes:set', setShowAxes);
+ return () => event?.unbind();
+ }, [observer]);
+
+ /**
+ * @param {string} text - The label.
+ * @param {string} path - The observer path.
+ * @param {number} min - The minimum.
+ * @param {number} max - The maximum.
+ * @param {number} [precision] - The precision.
+ * @returns {ReactElement} The slider.
+ */
+ const slider = (text, path, min, max, precision = 2) => (
+
+
+
+ );
+
+ /**
+ * @param {string} text - The label.
+ * @param {string} path - The observer path.
+ * @returns {ReactElement} The toggle.
+ */
+ const toggle = (text, path) => (
+
+
+
+ );
+
+ /**
+ * @param {string} text - The label.
+ * @param {string} path - The observer path.
+ * @returns {ReactElement} The color picker.
+ */
+ const color = (text, path) => (
+
+
+
+ );
+
+ return (
+ <>
+
+
+
+
+ {toggle('Enabled', 'grid.enabled')}
+
+
+
+ {slider('Spacing', 'grid.spacing', 0.1, 50, 1)}
+ {slider('Subdivisions', 'grid.subdivisions', 1, 10, 0)}
+
+
+ {toggle('Infinite', 'grid.infinite')}
+ {!infinite && (
+
+
+
+ )}
+ {slider('Fade Start', 'grid.fadeStart', 0, 1000, 0)}
+ {slider('Fade End', 'grid.fadeEnd', 0, 1000, 0)}
+
+
+ {color('Color X', 'grid.colorX')}
+ {color('Color Y', 'grid.colorY')}
+ {color('Color Z', 'grid.colorZ')}
+ {color('Major Line Color', 'grid.majorLineColor')}
+ {color('Minor Line Color', 'grid.minorLineColor')}
+
+
+ {toggle('Show Axes', 'grid.showAxes')}
+ {showAxes && slider('Axis Line Thickness', 'grid.axisLineThickness', 0.5, 5, 1)}
+
+
+ {slider('Major Line Thickness', 'grid.majorLineThickness', 0.5, 5, 1)}
+ {slider('Minor Line Thickness', 'grid.minorLineThickness', 0.5, 5, 1)}
+ {toggle('Contrast', 'grid.contrast')}
+
+
+ {slider('Depth Offset', 'grid.depthOffset', 0, 0.01, 4)}
+ {toggle('Depth Test', 'grid.depthTest')}
+ {toggle('Depth Write', 'grid.depthWrite')}
+ {toggle('Depth Dither', 'grid.depthDither')}
+
+
+
+
+
+ {slider('Far Clip', 'camera.farClip', 10, 2000, 0)}
+
+ >
+ );
+}
diff --git a/examples/src/examples/graphics/grid.example.mjs b/examples/src/examples/graphics/grid.example.mjs
new file mode 100644
index 00000000000..e0382b22d9d
--- /dev/null
+++ b/examples/src/examples/graphics/grid.example.mjs
@@ -0,0 +1,376 @@
+// @config
+//
+// A reference grid with every setting of the Grid script. Lower Depth Offset to see the dark plane
+// hide it, and pick the SuperSplat preset to see it hide gaussian splats with Depth Write.
+//
+// `LMB` Orbit · `RMB` Pan · `Wheel` / `Pinch` Zoom
+
+import {
+ AppBase,
+ AppOptions,
+ BoundingBox,
+ CameraComponentSystem,
+ Color,
+ Entity,
+ FILLMODE_FILL_WINDOW,
+ FloatPacking,
+ GSplatComponentSystem,
+ GSplatContainer,
+ GSplatFormat,
+ Keyboard,
+ LightComponentSystem,
+ Mouse,
+ PROJECTION_PERSPECTIVE,
+ RESOLUTION_AUTO,
+ RenderComponentSystem,
+ ScriptComponentSystem,
+ StandardMaterial,
+ Vec2,
+ Vec3,
+ createGraphicsDevice
+} from 'playcanvas';
+import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
+import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
+
+import { data, deviceType } from 'examples/context';
+
+/**
+ * @import { Material } from 'playcanvas'
+ */
+
+const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
+window.focus();
+
+const gfxOptions = {
+ deviceTypes: [deviceType],
+
+ // splats are expensive multisampled, and the grid antialiases itself
+ antialias: false
+};
+
+const device = await createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new AppOptions();
+createOptions.graphicsDevice = device;
+createOptions.mouse = new Mouse(document.body);
+createOptions.keyboard = new Keyboard(window);
+
+createOptions.componentSystems = [
+ RenderComponentSystem,
+ CameraComponentSystem,
+ LightComponentSystem,
+ ScriptComponentSystem,
+ GSplatComponentSystem
+];
+
+const app = new AppBase(canvas);
+app.init(createOptions);
+
+// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
+app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
+app.setCanvasResolution(RESOLUTION_AUTO);
+
+app.start();
+
+/**
+ * @param {Color} color - The color.
+ * @returns {Material} - The standard material.
+ */
+const createColorMaterial = (color) => {
+ const material = new StandardMaterial();
+ material.diffuse = color;
+ material.update();
+ return material;
+};
+
+// Scene settings
+app.scene.ambientLight = new Color(0.2, 0.2, 0.2);
+
+// Create entities, half through the grid
+const box = new Entity('box');
+box.addComponent('render', {
+ type: 'box',
+ material: createColorMaterial(new Color(0.8, 1, 1))
+});
+box.setPosition(1, 0, 1);
+app.root.addChild(box);
+
+// A splat sphere for the SuperSplat preset, colored by direction and shaded
+const SPLAT_COUNT = 6000;
+const SPLAT_SIZE = 0.025;
+const splatFormat = GSplatFormat.createSimpleFormat(device);
+const container = new GSplatContainer(device, SPLAT_COUNT, splatFormat);
+const centerTex = container.getTexture('dataCenter');
+const colorTex = container.getTexture('dataColor');
+const centerData = centerTex.lock();
+const colorData = colorTex.lock();
+const lightDir = new Vec3(0.5, 0.8, 0.3).normalize();
+const n = new Vec3();
+for (let i = 0; i < SPLAT_COUNT; i++) {
+ // fibonacci spiral
+ const y = 1 - (2 * (i + 0.5)) / SPLAT_COUNT;
+ const r = Math.sqrt(1 - y * y);
+ const a = i * Math.PI * (3 - Math.sqrt(5));
+ n.set(Math.cos(a) * r, y, Math.sin(a) * r);
+
+ // color by direction, lit from one side
+ const shade = 0.15 + 0.35 * Math.max(n.dot(lightDir), 0);
+ const rgb = [0.55 + 0.45 * n.x, 0.55 + 0.45 * n.y, 0.55 + 0.45 * n.z].map((c) => c * shade);
+
+ centerData.set([n.x * 0.5, n.y * 0.5, n.z * 0.5, SPLAT_SIZE], i * 4);
+ for (let c = 0; c < 3; c++) {
+ colorData[i * 4 + c] = FloatPacking.float2Half(rgb[c]);
+ }
+ colorData[i * 4 + 3] = FloatPacking.float2Half(1);
+ container.centers.set([n.x * 0.5, n.y * 0.5, n.z * 0.5], i * 3);
+}
+centerTex.unlock();
+colorTex.unlock();
+container.aabb = new BoundingBox(Vec3.ZERO, new Vec3(0.5 + SPLAT_SIZE, 0.5 + SPLAT_SIZE, 0.5 + SPLAT_SIZE));
+container.update(SPLAT_COUNT);
+
+const splatSphere = new Entity('splat-sphere');
+splatSphere.addComponent('gsplat', {
+ unified: true
+});
+splatSphere.gsplat.resource = container;
+splatSphere.setPosition(-1, 0, 1);
+splatSphere.enabled = false;
+app.root.addChild(splatSphere);
+
+const sphere = new Entity('sphere');
+sphere.addComponent('render', {
+ type: 'sphere',
+ material: createColorMaterial(new Color(1, 0.8, 1))
+});
+sphere.setPosition(-1, 0, 1);
+app.root.addChild(sphere);
+
+// A plane on the grid, hiding it without a Depth Offset
+const plane = new Entity('plane');
+plane.addComponent('render', {
+ type: 'plane',
+ material: createColorMaterial(new Color(0.35, 0.35, 0.3))
+});
+plane.setPosition(-1, 0, -1);
+plane.setLocalScale(1.5, 1, 1.5);
+app.root.addChild(plane);
+
+const capsule = new Entity('capsule');
+capsule.addComponent('render', {
+ type: 'capsule',
+ material: createColorMaterial(new Color(0.8, 0.8, 1))
+});
+capsule.setPosition(1, 0, -1);
+app.root.addChild(capsule);
+
+// Camera
+const camera = new Entity('camera');
+camera.addComponent('script');
+camera.addComponent('camera', {
+ clearColor: new Color(0.1, 0.1, 0.1),
+ farClip: 1000
+});
+const cameraOffset = 4 * camera.camera.aspectRatio;
+camera.setPosition(cameraOffset, cameraOffset, cameraOffset);
+app.root.addChild(camera);
+
+// Camera controls
+const cc = /** @type {CameraControls} */ (camera.script.create(CameraControls));
+Object.assign(cc, {
+ focusPoint: Vec3.ZERO,
+ sceneSize: 5,
+ rotateDamping: 0.95,
+ moveDamping: 0.95,
+ zoomDamping: 0.95,
+ pitchRange: new Vec2(-89.999, 89.999),
+ zoomRange: new Vec2(0.5, 500),
+ enableFly: false
+});
+
+// Create light entity
+const light = new Entity('light');
+light.addComponent('light');
+app.root.addChild(light);
+light.setEulerAngles(0, 0, -60);
+
+// Grid, sized by its scale when not infinite
+const gridEntity = new Entity('grid');
+gridEntity.setLocalScale(20, 1, 20);
+gridEntity.addComponent('script');
+app.root.addChild(gridEntity);
+const grid = /** @type {Grid} */ (gridEntity.script.create(Grid));
+
+data.set('grid', {
+ enabled: grid.enabled,
+ plane: grid.plane,
+ spacing: grid.spacing,
+ subdivisions: grid.subdivisions,
+ infinite: grid.infinite,
+ size: [20, 20],
+ majorLineThickness: grid.majorLineThickness,
+ minorLineThickness: grid.minorLineThickness,
+ axisLineThickness: grid.axisLineThickness,
+ showAxes: grid.showAxes,
+ contrast: grid.contrast,
+ colorX: grid.colorX.toArray(),
+ colorY: grid.colorY.toArray(),
+ colorZ: grid.colorZ.toArray(),
+ majorLineColor: grid.majorLineColor.toArray(),
+ minorLineColor: grid.minorLineColor.toArray(),
+ fadeStart: grid.fadeRange.x,
+ fadeEnd: grid.fadeRange.y,
+ depthOffset: grid.depthOffset,
+ depthTest: grid.depthTest,
+ depthWrite: grid.depthWrite,
+ depthDither: grid.depthDither
+});
+// Presets, applied through the panel so its controls follow
+const PRESETS = {
+ default: {
+ plane: 'xz',
+ spacing: 10,
+ subdivisions: 10,
+ infinite: true,
+ size: [20, 20],
+ fadeStart: 400,
+ fadeEnd: 1000,
+ showAxes: true,
+ axisLineThickness: 2,
+ majorLineThickness: 1.5,
+ minorLineThickness: 1,
+ contrast: false,
+ colorX: [1, 0.3, 0.3, 1],
+ colorY: [0.3, 1, 0.3, 1],
+ colorZ: [0.3, 0.3, 1, 1],
+ majorLineColor: [0.8, 0.8, 0.8, 0.6],
+ minorLineColor: [0.6, 0.6, 0.6, 0.4],
+ depthOffset: 0.001,
+ depthTest: true,
+ depthWrite: false,
+ depthDither: false
+ },
+ editor: {
+ plane: 'xz',
+ spacing: 1,
+ subdivisions: 1,
+ infinite: false,
+ size: [8, 8],
+ fadeStart: 400,
+ fadeEnd: 1000,
+ showAxes: true,
+ axisLineThickness: 1,
+ majorLineThickness: 1,
+ minorLineThickness: 1,
+ contrast: false,
+ colorX: [0, 0, 0, 1],
+ colorY: [0, 0, 0, 1],
+ colorZ: [0, 0, 0, 1],
+ majorLineColor: [136 / 255, 136 / 255, 136 / 255, 1],
+ minorLineColor: [136 / 255, 136 / 255, 136 / 255, 1],
+ depthOffset: 0,
+ depthTest: true,
+ depthWrite: false,
+ depthDither: false
+ },
+ supersplat: {
+ plane: 'xz',
+ spacing: 10,
+ subdivisions: 10,
+ infinite: true,
+ size: [20, 20],
+ fadeStart: 400,
+ fadeEnd: 1000,
+ showAxes: true,
+ axisLineThickness: 2,
+ majorLineThickness: 1.5,
+ minorLineThickness: 1,
+ contrast: false,
+ colorX: [1, 0.2, 0.2, 1],
+ colorY: [0.2, 1, 0.2, 1],
+ colorZ: [0.2, 0.2, 1, 1],
+ majorLineColor: [0.9, 0.9, 0.9, 1],
+ minorLineColor: [0.7, 0.7, 0.7, 1],
+ depthOffset: 0,
+ depthTest: true,
+ depthWrite: true,
+ depthDither: true
+ }
+};
+data.set('preset', 'default');
+
+data.set('camera', {
+ proj: PROJECTION_PERSPECTIVE + 1,
+ farClip: camera.camera.farClip
+});
+
+// Controls hook
+const COLORS = ['colorX', 'colorY', 'colorZ', 'majorLineColor', 'minorLineColor'];
+data.on('*:set', (/** @type {string} */ path, /** @type {any} */ value) => {
+ const [category, key] = path.split('.');
+ switch (category) {
+ case 'grid': {
+ // vector inputs set one component at a time
+ const v = data.get(`grid.${key}`);
+ if (key === 'size' || key === 'plane') {
+ // size is the entity's scale on the plane's axes
+ const [w, d] = data.get('grid.size');
+ const plane = data.get('grid.plane');
+ grid.plane = plane;
+ gridEntity.setLocalScale(
+ plane === 'yz' ? 1 : w,
+ plane === 'xz' ? 1 : plane === 'xy' ? d : w,
+ plane === 'xy' ? 1 : d
+ );
+ } else if (key === 'fadeStart' || key === 'fadeEnd') {
+ // the sliders set the range's ends
+ grid.fadeRange = new Vec2(data.get('grid.fadeStart'), data.get('grid.fadeEnd'));
+ } else if (key === 'contrast') {
+ // contrast needs the scene color map
+ camera.camera.renderSceneColorMap = v;
+ grid.contrast = v;
+ } else if (COLORS.includes(key)) {
+ // @ts-ignore
+ grid[key] = new Color().fromArray(v);
+ } else {
+ // @ts-ignore
+ grid[key] = v;
+ }
+ break;
+ }
+ case 'preset': {
+ const preset = PRESETS[/** @type {keyof typeof PRESETS} */ (value)];
+ for (const [name, v] of Object.entries(preset ?? {})) {
+ data.set(`grid.${name}`, v);
+ }
+
+ // splats only with the SuperSplat preset
+ splatSphere.enabled = value === 'supersplat';
+ sphere.enabled = !splatSphere.enabled;
+ break;
+ }
+ case 'camera': {
+ switch (key) {
+ case 'proj': {
+ camera.camera.projection = value - 1;
+ break;
+ }
+ case 'farClip': {
+ camera.camera.farClip = value;
+ break;
+ }
+ }
+ break;
+ }
+ }
+});
+
+// Ensure canvas is resized when window changes size
+const resize = () => app.resizeCanvas();
+window.addEventListener('resize', resize);
+resize();
+
+app.on('destroy', () => {
+ window.removeEventListener('resize', resize);
+});
diff --git a/examples/src/examples/misc/editor.controls.jsx b/examples/src/examples/misc/editor.controls.jsx
index 5210b93af64..7ac6f440d39 100644
--- a/examples/src/examples/misc/editor.controls.jsx
+++ b/examples/src/examples/misc/editor.controls.jsx
@@ -130,17 +130,6 @@ export function Controls({ observer }) {
)}
-
-
-
{
// Grid
const gridEntity = new Entity('grid');
-gridEntity.setLocalScale(8, 1, 8);
app.root.addChild(gridEntity);
gridEntity.addComponent('script');
const grid = /** @type {Grid} */ (gridEntity.script.create(Grid));
data.set('grid', {
colorX: Object.values(grid.colorX),
- colorZ: Object.values(grid.colorZ),
- resolution: grid.resolution + 1
+ colorZ: Object.values(grid.colorZ)
});
// Create light entity
@@ -342,9 +340,6 @@ data.on('*:set', (/** @type {string} */ path, /** @type {any} */ value) => {
case 'colorZ':
grid.colorZ = tmpC1.set(value[0], value[1], value[2]);
break;
- case 'resolution':
- grid.resolution = value - 1;
- break;
}
break;
}
diff --git a/examples/thumbnails/graphics_grid_large.webp b/examples/thumbnails/graphics_grid_large.webp
new file mode 100644
index 00000000000..448454e9348
Binary files /dev/null and b/examples/thumbnails/graphics_grid_large.webp differ
diff --git a/examples/thumbnails/graphics_grid_small.webp b/examples/thumbnails/graphics_grid_small.webp
new file mode 100644
index 00000000000..4630e31eaa7
Binary files /dev/null and b/examples/thumbnails/graphics_grid_small.webp differ
diff --git a/scripts/esm/grid.mjs b/scripts/esm/grid.mjs
index d71cca84434..2230838b33d 100644
--- a/scripts/esm/grid.mjs
+++ b/scripts/esm/grid.mjs
@@ -1,251 +1,21 @@
-import {
- ShaderMaterial,
- SEMANTIC_POSITION,
- SEMANTIC_TEXCOORD0,
- BLEND_NORMAL,
- CULLFACE_NONE,
- PlaneGeometry,
- Mesh,
- MeshInstance,
- Color,
- Script,
- Vec2
-} from 'playcanvas';
-
-const tmpVa = new Vec2();
-
-const EPSILON = 1e-3;
-
-const vertexGLSL = /* glsl */ `
- attribute vec3 vertex_position;
- attribute vec2 aUv0;
-
- uniform mat4 matrix_model;
- uniform mat4 matrix_viewProjection;
-
- varying vec2 uv0;
-
- void main(void) {
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
- uv0 = aUv0;
- }
-`;
-
-const fragmentGLSL = /* glsl */ `
- uniform vec2 uHalfExtents;
- uniform vec3 uColorX;
- uniform vec3 uColorZ;
- uniform int uResolution;
-
- varying vec2 uv0;
-
- // 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);
- }
-
- void main(void) {
- vec2 uv = uv0;
-
- vec2 pos = (uv * 2.0 - 1.0) * uHalfExtents;
- vec2 ddx = dFdx(pos);
- vec2 ddy = dFdy(pos);
-
- float epsilon = 1.0 / 255.0;
-
- vec2 levelPos;
- float levelSize;
- float levelAlpha;
-
- levelPos = pos * 0.1;
- levelSize = 2.0 / 1000.0;
- levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2(levelSize));
- if (levelAlpha > epsilon) {
- vec3 color;
- if (abs(levelPos.x) < levelSize) {
- if (abs(levelPos.y) < levelSize) {
- color = vec3(1.0);
- } else {
- color = uColorZ;
- }
- } else if (abs(levelPos.y) < levelSize) {
- color = uColorX;
- } else {
- color = vec3(0.9);
- }
- gl_FragColor = vec4(color, levelAlpha);
- return;
- }
-
- levelPos = pos;
- levelSize = 1.0 / 100.0;
- levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2(levelSize));
- if (levelAlpha > epsilon) {
- if (uResolution < 1) {
- discard;
- }
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
- return;
- }
-
- levelPos = pos * 10.0;
- levelSize = 1.0 / 100.0;
- levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2(levelSize));
- if (levelAlpha > epsilon) {
- if (uResolution < 2) {
- discard;
- }
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
- return;
- }
-
- discard;
- }
-`;
-
-const vertexWGSL = /* wgsl */ `
- attribute vertex_position: vec3f;
- attribute aUv0: vec2f;
-
- uniform matrix_model: mat4x4f;
- uniform matrix_viewProjection: mat4x4f;
-
- varying uv0: vec2f;
-
- @vertex
- fn vertexMain(input: VertexInput) -> VertexOutput {
- var output: VertexOutput;
- output.position = uniform.matrix_viewProjection * uniform.matrix_model * vec4f(input.vertex_position, 1.0);
- output.uv0 = input.aUv0;
- return output;
- }
-`;
-
-const fragmentWGSL = /* wgsl */ `
- uniform uHalfExtents: vec2f;
- uniform uColorX: vec3f;
- uniform uColorZ: vec3f;
- uniform uResolution: u32;
-
- varying uv0: vec2f;
-
- // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c
- fn pristineGrid(uv: vec2f, ddx: vec2f, ddy: vec2f, lineWidth: vec2f) -> f32 {
- let uvDeriv = vec2f(length(vec2f(ddx.x, ddy.x)), length(vec2f(ddx.y, ddy.y)));
- let invertLine = vec2(lineWidth.x > 0.5, lineWidth.y > 0.5);
- let targetWidth = vec2f(
- select(lineWidth.x, 1.0 - lineWidth.x, invertLine.x),
- select(lineWidth.y, 1.0 - lineWidth.y, invertLine.y)
- );
- let drawWidth = clamp(targetWidth, uvDeriv, vec2f(0.5));
- let lineAA = uvDeriv * 1.5;
- var gridUV = abs(fract(uv) * 2.0 - 1.0);
- gridUV.x = select(1.0 - gridUV.x, gridUV.x, invertLine.x);
- gridUV.y = select(1.0 - gridUV.y, gridUV.y, invertLine.y);
- var grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
-
- grid2 *= clamp(targetWidth / drawWidth, vec2f(0.0), vec2f(1.0));
- grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, vec2f(0.0), vec2f(1.0)));
- grid2.x = select(grid2.x, 1.0 - grid2.x, invertLine.x);
- grid2.y = select(grid2.y, 1.0 - grid2.y, invertLine.y);
-
- return mix(grid2.x, 1.0, grid2.y);
- }
-
- @fragment
- fn fragmentMain(input: FragmentInput) -> FragmentOutput {
- var output: FragmentOutput;
- let uv: vec2f = input.uv0;
-
- let pos: vec2f = (uv * 2.0 - 1.0) * uniform.uHalfExtents;
- let ddx: vec2f = dpdx(pos);
- let ddy: vec2f = dpdy(pos);
-
- let epsilon: f32 = 1.0 / 255.0;
-
- var levelPos: vec2f;
- var levelSize: f32;
- var levelAlpha: f32;
-
- levelPos = pos * 0.1;
- levelSize = 2.0 / 1000.0;
- levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2f(levelSize));
- if (levelAlpha > epsilon) {
- var color: vec3f;
- if (abs(levelPos.x) < levelSize) {
- if (abs(levelPos.y) < levelSize) {
- color = vec3f(1.0);
- } else {
- color = uniform.uColorZ;
- }
- } else if (abs(levelPos.y) < levelSize) {
- color = uniform.uColorX;
- } else {
- color = vec3f(0.9);
- }
- output.color = vec4f(color, levelAlpha);
- return output;
- }
-
- levelPos = pos;
- levelSize = 1.0 / 100.0;
- levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2f(levelSize));
- if (levelAlpha > epsilon) {
- if (uniform.uResolution < 1) {
- discard;
- }
- output.color = vec4f(vec3f(0.7), levelAlpha);
- return output;
- }
+import { Color, Grid as EngineGrid, Script, Vec2 } from 'playcanvas';
- levelPos = pos * 10.0;
- levelSize = 1.0 / 100.0;
- levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2f(levelSize));
- if (levelAlpha > epsilon) {
- if (uniform.uResolution < 2) {
- discard;
- }
- output.color = vec4f(vec3f(0.7), levelAlpha);
- return output;
- }
+// warn once about the deprecated resolution
+let resolutionWarned = false;
- discard;
- return output;
- }
-`;
+/**
+ * @import { AppBase, Entity } from 'playcanvas'
+ */
/**
- * Renders an anti-aliased reference grid on the XZ plane, using the "pristine grid" shader
- * technique. Lines along the X and Z axes are tinted with {@link Grid#colorX} and
- * {@link Grid#colorZ}, and {@link Grid#resolution} selects how many grid levels are shown.
- *
- * The script creates a render component on its entity, with the grid plane sized by the
- * entity's local scale.
+ * Renders the engine's {@link EngineGrid | Grid} through a render component on its entity. The
+ * entity's scale sizes the grid when it isn't {@link Grid#infinite}.
*
* @example
- * gridEntity.setLocalScale(8, 1, 8);
* gridEntity.addComponent('script');
* gridEntity.script.create(Grid, {
* properties: {
- * resolution: Grid.RESOLUTION_HIGH
+ * spacing: 1
* }
* });
* @category Rendering
@@ -255,54 +25,47 @@ class Grid extends Script {
/**
* @type {number}
+ * @deprecated Has no effect, as the levels follow the zoom.
+ * @ignore
*/
static RESOLUTION_LOW = 0;
/**
* @type {number}
+ * @deprecated Has no effect, as the levels follow the zoom.
+ * @ignore
*/
static RESOLUTION_MEDIUM = 1;
/**
* @type {number}
+ * @deprecated Has no effect, as the levels follow the zoom.
+ * @ignore
*/
static RESOLUTION_HIGH = 2;
/**
- * @type {ShaderMaterial}
+ * The engine grid this script drives.
+ *
+ * @type {EngineGrid}
* @private
*/
- _material;
+ _grid = new EngineGrid(this.app.graphicsDevice);
/**
- * @type {MeshInstance}
- * @private
+ * @param {{ app: AppBase, entity: Entity }} args - The script arguments.
*/
- _meshInstance;
+ constructor(args) {
+ super(args);
- /**
- * @type {Vec2}
- * @private
- */
- _halfExtents = new Vec2();
-
- /**
- * @type {Color}
- * @private
- */
- _colorX = new Color(1, 0.3, 0.3);
-
- /**
- * @type {Color}
- * @private
- */
- _colorZ = new Color(0.3, 0.3, 1);
-
- /**
- * @type {number}
- * @private
- */
- _resolution = Grid.RESOLUTION_HIGH;
+ // release the grid even if the script never initializes
+ this.on('destroy', () => {
+ if (this.entity.render?.meshInstances === this._grid.meshInstances) {
+ this.entity.removeComponent('render');
+ }
+ this._grid.destroy();
+ });
+ }
initialize() {
// check if the entity already has a render component
@@ -311,156 +74,334 @@ class Grid extends Script {
return;
}
- // create render component
this.entity.addComponent('render', {
+ meshInstances: this._grid.meshInstances,
castShadows: false
});
- // create shader material
- this._material = new ShaderMaterial({
- uniqueName: 'grid-shader',
- vertexGLSL: vertexGLSL,
- fragmentGLSL: fragmentGLSL,
- vertexWGSL: vertexWGSL,
- fragmentWGSL: fragmentWGSL,
- attributes: {
- vertex_position: SEMANTIC_POSITION,
- aUv0: SEMANTIC_TEXCOORD0
- }
- });
- this._material.blendType = BLEND_NORMAL;
- this._material.cull = CULLFACE_NONE;
- this._material.update();
-
- // create mesh
- const mesh = Mesh.fromGeometry(this.app.graphicsDevice, new PlaneGeometry());
- this._meshInstance = new MeshInstance(mesh, this._material);
- this._meshInstance.pick = false;
- this.entity.render.meshInstances = [this._meshInstance];
-
- // set the initial values
- this.colorX = this._colorX;
- this.colorZ = this._colorZ;
- this.resolution = this._resolution;
-
- // calculate half extents
- this._set('uHalfExtents', this._calcHalfExtents(tmpVa));
-
- // update the half extents when the entity scale changes
- this.app.on('prerender', () => {
- if (!this.enabled) {
- return;
- }
- const halfExtents = this._calcHalfExtents(tmpVa);
- if (this._halfExtents.distance(halfExtents) > EPSILON) {
- this._set('uHalfExtents', halfExtents);
- }
- });
-
- // enable/disable the mesh instance
this.on('enable', () => {
- this._meshInstance.visible = true;
+ this._grid.visible = true;
});
this.on('disable', () => {
- this._meshInstance.visible = false;
+ this._grid.visible = false;
});
+ }
- this.on('destroy', this.destroy, this);
+ /**
+ * @attribute
+ * @title Plane
+ * @description The plane in the entity's local space: 'xz', 'xy' or 'yz'.
+ * @type {string}
+ * @default xz
+ */
+ set plane(value) {
+ this._grid.plane = value;
+ }
+
+ get plane() {
+ return this._grid.plane;
}
/**
- * @param {Vec2} vec - The vector to copy the half extents to.
- * @returns {Vec2} - The half extents.
- * @private
+ * @attribute
+ * @title Spacing
+ * @description The cell size the levels are scaled from, in world units.
+ * @type {number}
+ * @default 10
*/
- _calcHalfExtents(vec) {
- const scale = this.entity.getLocalScale();
- return vec.set(scale.x / 2, scale.z / 2);
+ set spacing(value) {
+ this._grid.spacing = value;
+ }
+
+ get spacing() {
+ return this._grid.spacing;
}
/**
- * @param {string} name - The name of the parameter.
- * @param {Color|Vec2|number} value - The value of the parameter.
- * @private
+ * @attribute
+ * @title Subdivisions
+ * @description How many cells of a level fit across one of the level above, or 1 for a
+ * single fixed level.
+ * @type {number}
+ * @default 10
*/
- _set(name, value) {
- if (!this._material) {
- return;
- }
+ set subdivisions(value) {
+ this._grid.subdivisions = value;
+ }
- if (value instanceof Color) {
- this._material.setParameter(name, [value.r, value.g, value.b]);
- }
+ get subdivisions() {
+ return this._grid.subdivisions;
+ }
+
+ /**
+ * @attribute
+ * @title Infinite
+ * @description Whether the grid has no edge. When false, the entity's scale sizes it.
+ * @type {boolean}
+ * @default true
+ */
+ set infinite(value) {
+ this._grid.infinite = value;
+ }
+
+ get infinite() {
+ return this._grid.infinite;
+ }
+
+ /**
+ * @attribute
+ * @title Show Axes
+ * @description Whether the lines through the origin use the axis colors.
+ * @type {boolean}
+ * @default true
+ */
+ set showAxes(value) {
+ this._grid.showAxes = value;
+ }
+
+ get showAxes() {
+ return this._grid.showAxes;
+ }
+
+ /**
+ * @attribute
+ * @title Axis Line Thickness
+ * @description The axis line thickness in CSS pixels.
+ * @type {number}
+ * @default 2
+ */
+ set axisLineThickness(value) {
+ this._grid.axisLineThickness = value;
+ }
+
+ get axisLineThickness() {
+ return this._grid.axisLineThickness;
+ }
+
+ /**
+ * @attribute
+ * @title Major Line Thickness
+ * @description The major line thickness in CSS pixels.
+ * @type {number}
+ * @default 1.5
+ */
+ set majorLineThickness(value) {
+ this._grid.majorLineThickness = value;
+ }
+
+ get majorLineThickness() {
+ return this._grid.majorLineThickness;
+ }
+
+ /**
+ * @attribute
+ * @title Minor Line Thickness
+ * @description The minor line thickness in CSS pixels.
+ * @type {number}
+ * @default 1
+ */
+ set minorLineThickness(value) {
+ this._grid.minorLineThickness = value;
+ }
+ get minorLineThickness() {
+ return this._grid.minorLineThickness;
+ }
+
+ /**
+ * @attribute
+ * @title Fade Range
+ * @description The distances from the camera where the grid starts and finishes fading out.
+ * @type {Vec2}
+ * @default [400, 1000]
+ */
+ set fadeRange(value) {
if (value instanceof Vec2) {
- this._material.setParameter(name, [value.x, value.y]);
+ this._grid.fadeRange = value;
}
+ }
- if (typeof value === 'number') {
- this._material.setParameter(name, value);
- }
+ get fadeRange() {
+ return this._grid.fadeRange;
+ }
- this._material.update();
+ /**
+ * @attribute
+ * @title Contrast
+ * @description Whether the lines are black over light parts of the scene and white over dark
+ * ones. Needs the camera's scene color map.
+ * @type {boolean}
+ * @default false
+ */
+ set contrast(value) {
+ this._grid.contrast = value;
+ }
- this._meshInstance.material = this._material;
+ get contrast() {
+ return this._grid.contrast;
}
/**
* @attribute
- * @title Grid Color X
- * @description The color of the grid lines along the X axis.
+ * @title Axis Color X
+ * @description The X axis line color.
* @type {Color}
* @default [1, 0.3, 0.3, 1]
*/
set colorX(value) {
- if (!(value instanceof Color)) {
- return;
+ if (value instanceof Color) {
+ this._grid.setTheme({ axis: { x: value } });
}
- this._colorX.copy(value);
- this._set('uColorX', this._colorX);
}
get colorX() {
- return this._colorX;
+ return this._grid.theme.axis.x;
}
/**
* @attribute
- * @title Grid Color Z
- * @description The color of the grid lines along the Z axis.
+ * @title Axis Color Y
+ * @description The Y axis line color.
+ * @type {Color}
+ * @default [0.3, 1, 0.3, 1]
+ */
+ set colorY(value) {
+ if (value instanceof Color) {
+ this._grid.setTheme({ axis: { y: value } });
+ }
+ }
+
+ get colorY() {
+ return this._grid.theme.axis.y;
+ }
+
+ /**
+ * @attribute
+ * @title Axis Color Z
+ * @description The Z axis line color.
* @type {Color}
* @default [0.3, 0.3, 1, 1]
*/
set colorZ(value) {
- if (!(value instanceof Color)) {
- return;
+ if (value instanceof Color) {
+ this._grid.setTheme({ axis: { z: value } });
}
- this._colorZ.copy(value);
- this._set('uColorZ', this._colorZ);
}
get colorZ() {
- return this._colorZ;
+ return this._grid.theme.axis.z;
+ }
+
+ /**
+ * @attribute
+ * @title Major Line Color
+ * @description The major line color. The alpha sets the opacity.
+ * @type {Color}
+ * @default [0.8, 0.8, 0.8, 0.6]
+ */
+ set majorLineColor(value) {
+ if (value instanceof Color) {
+ this._grid.setTheme({ major: value });
+ }
+ }
+
+ get majorLineColor() {
+ return this._grid.theme.major;
+ }
+
+ /**
+ * @attribute
+ * @title Minor Line Color
+ * @description The minor line color. The alpha sets the opacity.
+ * @type {Color}
+ * @default [0.6, 0.6, 0.6, 0.4]
+ */
+ set minorLineColor(value) {
+ if (value instanceof Color) {
+ this._grid.setTheme({ minor: value });
+ }
+ }
+
+ get minorLineColor() {
+ return this._grid.theme.minor;
}
/**
* @attribute
- * @title Grid Resolution
- * @description The resolution of the grid.
+ * @title Depth Offset
+ * @description The fraction of the camera distance the grid's depth moves toward the camera
+ * by, so a coplanar surface doesn't hide it.
* @type {number}
- * @default 2
+ * @default 0.001
*/
- set resolution(value) {
- this._resolution = value;
- this._set('uResolution', this._resolution);
+ set depthOffset(value) {
+ this._grid.depthOffset = value;
}
- get resolution() {
- return this._resolution;
+ get depthOffset() {
+ return this._grid.depthOffset;
}
- destroy() {
- this.entity.removeComponent('render');
+ /**
+ * @attribute
+ * @title Depth Test
+ * @description Whether the grid is depth tested against the scene.
+ * @type {boolean}
+ * @default true
+ */
+ set depthTest(value) {
+ this._grid.depthTest = value;
+ }
+
+ get depthTest() {
+ return this._grid.depthTest;
+ }
+
+ /**
+ * @attribute
+ * @title Depth Write
+ * @description Whether the lines write depth, hiding transparent meshes and gaussian splats
+ * behind them.
+ * @type {boolean}
+ * @default false
+ */
+ set depthWrite(value) {
+ this._grid.depthWrite = value;
+ }
+
+ get depthWrite() {
+ return this._grid.depthWrite;
+ }
+
+ /**
+ * @attribute
+ * @title Depth Dither
+ * @description Whether the written depth is dithered by line coverage, for gaussian splats.
+ * @type {boolean}
+ * @default false
+ */
+ set depthDither(value) {
+ this._grid.depthDither = value;
+ }
+
+ get depthDither() {
+ return this._grid.depthDither;
+ }
+
+ /**
+ * @type {number}
+ * @deprecated Has no effect, as the levels follow the zoom.
+ * @ignore
+ */
+ set resolution(value) {
+ if (!resolutionWarned) {
+ resolutionWarned = true;
+ console.warn('Grid#resolution is deprecated and has no effect, as the levels follow the zoom.');
+ }
+ }
+
+ get resolution() {
+ return Grid.RESOLUTION_HIGH;
}
}
diff --git a/src/extras/grid/grid.js b/src/extras/grid/grid.js
new file mode 100644
index 00000000000..0b0f47f01e4
--- /dev/null
+++ b/src/extras/grid/grid.js
@@ -0,0 +1,716 @@
+import { Color } from '../../core/math/color.js';
+import { Vec2 } from '../../core/math/vec2.js';
+import { CULLFACE_NONE, SEMANTIC_POSITION } from '../../platform/graphics/constants.js';
+import { BLEND_NORMAL } from '../../scene/constants.js';
+import { PlaneGeometry } from '../../scene/geometry/plane-geometry.js';
+import { ShaderMaterial } from '../../scene/materials/shader-material.js';
+import { MeshInstance } from '../../scene/mesh-instance.js';
+import { Mesh } from '../../scene/mesh.js';
+import { fragmentGLSL, fragmentWGSL, vertexGLSL, vertexWGSL } from './shaders.js';
+
+/**
+ * @import { CameraComponent } from '../../framework/components/camera/component.js'
+ * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
+ */
+
+/**
+ * @typedef {object} GridTheme
+ * @property {{ x: Color, y: Color, z: Color }} axis - The axis line colors.
+ * @property {Color} major - The major line color.
+ * @property {Color} minor - The minor line color.
+ */
+
+/**
+ * The plane of a {@link Grid} in its node's local space: 'xz', 'xy' or 'yz'.
+ *
+ * @typedef {'xz' | 'xy' | 'yz'} GridPlane
+ */
+
+// local axes of each plane's u and v coordinates
+const PLANES = {
+ xz: [[1, 0, 0], [0, 0, 1]],
+ xy: [[1, 0, 0], [0, 1, 0]],
+ yz: [[0, 1, 0], [0, 0, 1]]
+};
+
+/**
+ * An anti-aliased reference grid whose levels of lines follow the zoom. It follows the camera and
+ * fades out with distance, or is sized by its node's scale when not {@link Grid#infinite}. It lies
+ * at its node's position and rotation; use {@link Grid#spacing} to size the cells.
+ *
+ * ```javascript
+ * const grid = new Grid(app.graphicsDevice);
+ * entity.addComponent('render', {
+ * meshInstances: grid.meshInstances,
+ * castShadows: false
+ * });
+ * ```
+ *
+ * Relevant Engine API examples:
+ *
+ * - [Grid](https://playcanvas.github.io/#/graphics/grid)
+ *
+ * @category Graphics
+ */
+class Grid {
+ /**
+ * @type {GraphicsDevice}
+ * @private
+ */
+ _device;
+
+ /**
+ * @type {ShaderMaterial}
+ * @private
+ */
+ _material;
+
+ /**
+ * Writes the depth of the lines.
+ *
+ * @type {ShaderMaterial}
+ * @private
+ */
+ _depthMaterial;
+
+ /**
+ * @type {MeshInstance[]}
+ * @private
+ */
+ _meshInstances;
+
+ /**
+ * @type {GridTheme}
+ * @private
+ */
+ _theme = {
+ axis: {
+ x: new Color(1, 0.3, 0.3),
+ y: new Color(0.3, 1, 0.3),
+ z: new Color(0.3, 0.3, 1)
+ },
+ major: new Color(0.8, 0.8, 0.8, 0.6),
+ minor: new Color(0.6, 0.6, 0.6, 0.4)
+ };
+
+ /**
+ * @type {GridPlane}
+ * @private
+ */
+ _plane = 'xz';
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _showAxes = true;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _contrast = false;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _spacing = 10;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _subdivisions = 10;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _majorLineThickness = 1.5;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _minorLineThickness = 1;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _axisLineThickness = 2;
+
+ /**
+ * CSS pixels to device pixels.
+ *
+ * @type {number}
+ * @private
+ */
+ _pixelRatio = 1;
+
+ /**
+ * @type {Vec2}
+ * @private
+ */
+ _fadeRange = new Vec2(400, 1000);
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _infinite = true;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _depthOffset = 0.001;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _depthTest = true;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _depthWrite = false;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _depthDither = false;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _visible = true;
+
+ /**
+ * Creates a new Grid instance.
+ *
+ * @param {GraphicsDevice} device - The graphics device.
+ * @example
+ * const grid = new Grid(app.graphicsDevice);
+ * grid.spacing = 1;
+ * grid.plane = 'xy';
+ */
+ constructor(device) {
+ this._device = device;
+ this._updatePixelRatio = this._updatePixelRatio.bind(this);
+ device.on('resizecanvas', this._updatePixelRatio);
+
+ this._material = this._createMaterial(false);
+ this._depthMaterial = this._createMaterial(true);
+
+ // moved in the vertex shader, so never culled; drawn first of the transparents so its depth
+ // can hide them
+ const mesh = Mesh.fromGeometry(device, new PlaneGeometry());
+ this._meshInstances = [this._material, this._depthMaterial].map((material) => {
+ const meshInstance = new MeshInstance(mesh, material);
+ meshInstance.cull = false;
+ meshInstance.pick = false;
+ meshInstance.drawBucket = 255;
+ return meshInstance;
+ });
+
+ this._updatePlane();
+ this._updateColor('uMajorColor', this._theme.major);
+ this._updateColor('uMinorColor', this._theme.minor);
+ this._setParameter('uSpacing', this._spacing);
+ this._updateLevels();
+ this._updatePixelRatio();
+ this._updateFade();
+ this._updateInfinite();
+ this._setParameter('uShowAxes', 1);
+ this._setParameter('uDepthOffset', this._depthOffset);
+ this._updateVisible();
+ }
+
+ /**
+ * Gets the mesh instances to add to a render component or layer: the lines and their depth
+ * pass. They have no node, so set one when adding them to a layer directly.
+ *
+ * @type {MeshInstance[]}
+ */
+ get meshInstances() {
+ return this._meshInstances;
+ }
+
+ /**
+ * Gets the theme.
+ *
+ * @type {GridTheme}
+ */
+ get theme() {
+ return this._theme;
+ }
+
+ /**
+ * Sets the theme or part of it. Each color's alpha sets its lines' opacity.
+ *
+ * @param {{ axis?: Partial, major?: Color, minor?: Color }} partial - The
+ * theme to set.
+ * @example
+ * grid.setTheme({
+ * axis: { x: new Color(1, 0, 0) },
+ * minor: new Color(0.5, 0.5, 0.5, 0.5)
+ * });
+ */
+ setTheme(partial) {
+ const theme = this._theme;
+ for (const axis of /** @type {const} */ (['x', 'y', 'z'])) {
+ const color = partial?.axis?.[axis];
+ if (color instanceof Color) {
+ theme.axis[axis].copy(color);
+ }
+ }
+ if (partial?.major instanceof Color) {
+ theme.major.copy(partial.major);
+ }
+ if (partial?.minor instanceof Color) {
+ theme.minor.copy(partial.minor);
+ }
+ this._updatePlane();
+ this._updateColor('uMajorColor', theme.major);
+ this._updateColor('uMinorColor', theme.minor);
+ }
+
+ /**
+ * Sets the plane in the node's local space. Defaults to 'xz'.
+ *
+ * @type {GridPlane}
+ */
+ set plane(value) {
+ if (!PLANES[value]) {
+ return;
+ }
+ this._plane = value;
+ this._updatePlane();
+ }
+
+ /**
+ * Gets the plane.
+ *
+ * @type {GridPlane}
+ */
+ get plane() {
+ return this._plane;
+ }
+
+ /**
+ * Sets whether the lines through the origin use the axis colors. Defaults to true.
+ *
+ * @type {boolean}
+ */
+ set showAxes(value) {
+ this._showAxes = !!value;
+ this._setParameter('uShowAxes', this._showAxes ? 1 : 0);
+ }
+
+ /**
+ * Gets whether the lines through the origin use the axis colors.
+ *
+ * @type {boolean}
+ */
+ get showAxes() {
+ return this._showAxes;
+ }
+
+ /**
+ * Sets whether the lines are black over light parts of the scene and white over dark ones,
+ * instead of their theme colors. Needs the scene color map, see
+ * {@link CameraComponent#requestSceneColorMap}. Defaults to false.
+ *
+ * @type {boolean}
+ */
+ set contrast(value) {
+ this._contrast = !!value;
+ this._material.setDefine('GRID_CONTRAST', this._contrast);
+ this._material.update();
+ }
+
+ /**
+ * Gets whether the lines contrast with the scene.
+ *
+ * @type {boolean}
+ */
+ get contrast() {
+ return this._contrast;
+ }
+
+ /**
+ * Sets the cell size the levels are scaled from, in world units. Defaults to 10.
+ *
+ * @type {number}
+ */
+ set spacing(value) {
+ this._spacing = Math.max(value, 1e-6);
+ this._setParameter('uSpacing', this._spacing);
+ }
+
+ /**
+ * Gets the cell size the levels are scaled from.
+ *
+ * @type {number}
+ */
+ get spacing() {
+ return this._spacing;
+ }
+
+ /**
+ * Sets how many cells of a level fit across one of the level above, or 1 for a single fixed
+ * level. Defaults to 10.
+ *
+ * @type {number}
+ */
+ set subdivisions(value) {
+ this._subdivisions = Math.max(value, 1);
+ this._updateLevels();
+ }
+
+ /**
+ * Gets how many cells of a level fit across one of the level above.
+ *
+ * @type {number}
+ */
+ get subdivisions() {
+ return this._subdivisions;
+ }
+
+ /**
+ * Sets the major line thickness in CSS pixels. Defaults to 1.5.
+ *
+ * @type {number}
+ */
+ set majorLineThickness(value) {
+ this._majorLineThickness = value;
+ this._updateThickness();
+ }
+
+ /**
+ * Gets the major line thickness in CSS pixels.
+ *
+ * @type {number}
+ */
+ get majorLineThickness() {
+ return this._majorLineThickness;
+ }
+
+ /**
+ * Sets the minor line thickness in CSS pixels. Defaults to 1.
+ *
+ * @type {number}
+ */
+ set minorLineThickness(value) {
+ this._minorLineThickness = value;
+ this._updateThickness();
+ }
+
+ /**
+ * Gets the minor line thickness in CSS pixels.
+ *
+ * @type {number}
+ */
+ get minorLineThickness() {
+ return this._minorLineThickness;
+ }
+
+ /**
+ * Sets the axis line thickness in CSS pixels. Defaults to 2.
+ *
+ * @type {number}
+ */
+ set axisLineThickness(value) {
+ this._axisLineThickness = value;
+ this._updateThickness();
+ }
+
+ /**
+ * Gets the axis line thickness in CSS pixels.
+ *
+ * @type {number}
+ */
+ get axisLineThickness() {
+ return this._axisLineThickness;
+ }
+
+ /**
+ * Sets the distances from the camera where the grid starts and finishes fading out. The end is
+ * kept within 90% of the far clip. Defaults to [400, 1000].
+ *
+ * @type {Vec2}
+ */
+ set fadeRange(value) {
+ this._fadeRange.copy(value);
+ this._updateFade();
+ }
+
+ /**
+ * Gets the distances from the camera where the grid starts and finishes fading out.
+ *
+ * @type {Vec2}
+ */
+ get fadeRange() {
+ return this._fadeRange;
+ }
+
+ /**
+ * Sets whether the grid has no edge. When false, it is sized by its node's scale along the
+ * plane's axes. Defaults to true.
+ *
+ * @type {boolean}
+ */
+ set infinite(value) {
+ this._infinite = !!value;
+ this._updateInfinite();
+ }
+
+ /**
+ * Gets whether the grid has no edge.
+ *
+ * @type {boolean}
+ */
+ get infinite() {
+ return this._infinite;
+ }
+
+ /**
+ * Sets the fraction of the camera distance the grid's depth moves toward the camera by, so a
+ * coplanar surface doesn't hide it. Defaults to 0.001.
+ *
+ * @type {number}
+ */
+ set depthOffset(value) {
+ this._depthOffset = value;
+ this._setParameter('uDepthOffset', value);
+ }
+
+ /**
+ * Gets the fraction of the camera distance the grid's depth moves toward the camera by.
+ *
+ * @type {number}
+ */
+ get depthOffset() {
+ return this._depthOffset;
+ }
+
+ /**
+ * Sets whether the grid is depth tested against the scene. Defaults to true.
+ *
+ * @type {boolean}
+ */
+ set depthTest(value) {
+ this._depthTest = !!value;
+ this._material.depthTest = this._depthTest;
+ this._material.update();
+ this._depthMaterial.depthTest = this._depthTest;
+ this._depthMaterial.update();
+ }
+
+ /**
+ * Gets whether the grid is depth tested against the scene.
+ *
+ * @type {boolean}
+ */
+ get depthTest() {
+ return this._depthTest;
+ }
+
+ /**
+ * Sets whether the lines write depth, hiding transparent meshes and gaussian splats behind
+ * them. Defaults to false.
+ *
+ * @type {boolean}
+ */
+ set depthWrite(value) {
+ this._depthWrite = !!value;
+ this._updateVisible();
+ }
+
+ /**
+ * Gets whether the lines write depth.
+ *
+ * @type {boolean}
+ */
+ get depthWrite() {
+ return this._depthWrite;
+ }
+
+ /**
+ * Sets whether the written depth is dithered by line coverage, so faint lines partly hide what
+ * is behind them. Suits gaussian splats. Defaults to false.
+ *
+ * @type {boolean}
+ */
+ set depthDither(value) {
+ this._depthDither = !!value;
+ this._depthMaterial.setDefine('GRID_DEPTH_DITHER', this._depthDither);
+ this._depthMaterial.update();
+ }
+
+ /**
+ * Gets whether the written depth is dithered.
+ *
+ * @type {boolean}
+ */
+ get depthDither() {
+ return this._depthDither;
+ }
+
+ /**
+ * Sets whether the grid is visible. Defaults to true.
+ *
+ * @type {boolean}
+ */
+ set visible(value) {
+ this._visible = !!value;
+ this._updateVisible();
+ }
+
+ /**
+ * Gets whether the grid is visible.
+ *
+ * @type {boolean}
+ */
+ get visible() {
+ return this._visible;
+ }
+
+ /**
+ * Creates a grid material.
+ *
+ * @param {boolean} depth - Whether it writes depth instead of color.
+ * @returns {ShaderMaterial} The material.
+ * @private
+ */
+ _createMaterial(depth) {
+ const material = new ShaderMaterial({
+ uniqueName: 'grid',
+ vertexGLSL,
+ fragmentGLSL,
+ vertexWGSL,
+ fragmentWGSL,
+ attributes: {
+ vertex_position: SEMANTIC_POSITION
+ }
+ });
+ // both blended, so they draw after the opaque meshes
+ material.cull = CULLFACE_NONE;
+ material.blendType = BLEND_NORMAL;
+ material.depthWrite = depth;
+ if (depth) {
+ material.setDefine('GRID_DEPTH', true);
+ material.redWrite = false;
+ material.greenWrite = false;
+ material.blueWrite = false;
+ material.alphaWrite = false;
+ }
+ material.update();
+ return material;
+ }
+
+ /**
+ * Sets a parameter on both materials.
+ *
+ * @param {string} name - The name of the parameter.
+ * @param {number|number[]} value - The value of the parameter.
+ * @private
+ */
+ _setParameter(name, value) {
+ this._material.setParameter(name, value);
+ this._depthMaterial.setParameter(name, value);
+ }
+
+ /**
+ * @param {string} name - The name of the parameter.
+ * @param {Color} color - The color.
+ * @private
+ */
+ _updateColor(name, color) {
+ this._setParameter(name, [color.r, color.g, color.b, color.a]);
+ }
+
+ /**
+ * @private
+ */
+ _updatePlane() {
+ const plane = this._plane;
+ const { axis } = this._theme;
+ const [u, v] = PLANES[plane];
+ this._setParameter('uAxisU', u);
+ this._setParameter('uAxisV', v);
+ this._updateColor('uColorU', plane === 'yz' ? axis.y : axis.x);
+ this._updateColor('uColorV', plane === 'xy' ? axis.y : axis.z);
+ }
+
+ /**
+ * @private
+ */
+ _updateLevels() {
+ this._setParameter('uSubdivisions', this._subdivisions);
+ }
+
+ /**
+ * @private
+ */
+ _updatePixelRatio() {
+ const device = this._device;
+ device.updateClientRect();
+ const { width } = device.clientRect;
+ this._pixelRatio = width > 0 ? device.width / width : 1;
+ this._setParameter('uPixelRatio', this._pixelRatio);
+ this._updateThickness();
+ }
+
+ /**
+ * @private
+ */
+ _updateThickness() {
+ const ratio = this._pixelRatio;
+ this._setParameter('uThickness', [
+ this._majorLineThickness * ratio,
+ this._minorLineThickness * ratio,
+ this._axisLineThickness * ratio
+ ]);
+ }
+
+ /**
+ * @private
+ */
+ _updateFade() {
+ this._setParameter('uFade', [this._fadeRange.x, this._fadeRange.y]);
+ }
+
+ /**
+ * @private
+ */
+ _updateInfinite() {
+ this._setParameter('uInfinite', this._infinite ? 1 : 0);
+ }
+
+ /**
+ * @private
+ */
+ _updateVisible() {
+ this._meshInstances[0].visible = this._visible;
+ this._meshInstances[1].visible = this._visible && this._depthWrite;
+ }
+
+ /**
+ * Destroys the mesh instances and materials. Remove the mesh instances from use first.
+ */
+ destroy() {
+ this._device.off('resizecanvas', this._updatePixelRatio);
+ this._meshInstances.forEach(meshInstance => meshInstance.destroy());
+ this._material.destroy();
+ this._depthMaterial.destroy();
+ }
+}
+
+export { Grid };
diff --git a/src/extras/grid/shaders.js b/src/extras/grid/shaders.js
new file mode 100644
index 00000000000..acf336fc198
--- /dev/null
+++ b/src/extras/grid/shaders.js
@@ -0,0 +1,381 @@
+// fraction of the far clip the fade must end within
+const FAR_FRACTION = '0.9';
+
+// cell size in CSS pixels below which lines thin with their cells, so distant lines fade
+const LINE_FADE_CELL = '64.0';
+
+// cell size in CSS pixels where a level starts to fade in, and the levels drawn above it
+const LEVEL_FADE_CELL = '4.0';
+const LEVELS_ABOVE = 2;
+
+export const vertexGLSL = /* glsl */`
+ attribute vec3 vertex_position;
+
+ uniform mat4 matrix_model;
+ uniform mat4 matrix_view;
+ uniform mat4 matrix_viewProjection;
+ uniform vec3 view_position;
+ uniform vec4 camera_params;
+
+ uniform vec3 uAxisU;
+ uniform vec3 uAxisV;
+ uniform float uInfinite;
+ uniform vec2 uFade;
+ uniform float uDepthOffset;
+
+ varying vec2 vGridPos;
+ varying vec3 vWorldPos;
+ varying vec4 vClipPos;
+ varying vec3 vToEye;
+ varying vec3 vNormal;
+
+ void main(void) {
+
+ // the node's scale sizes a fixed grid, never its cells
+ vec3 origin = matrix_model[3].xyz;
+ vec3 axisU = mat3(matrix_model) * uAxisU;
+ vec3 axisV = mat3(matrix_model) * uAxisV;
+ vec3 u = normalize(axisU);
+ vec3 v = normalize(axisV);
+
+ // an infinite grid follows the camera and spans the fade distance
+ float reach = 2.0 * min(uFade.y, camera_params.y * ${FAR_FRACTION});
+ vec2 center = vec2(dot(view_position - origin, u), dot(view_position - origin, v)) * uInfinite;
+ vGridPos = center + vertex_position.xz * mix(vec2(length(axisU), length(axisV)), vec2(reach), uInfinite);
+ vWorldPos = origin + u * vGridPos.x + v * vGridPos.y;
+
+ // offset along the view ray, so only depth changes
+ vec3 back = vec3(matrix_view[0][2], matrix_view[1][2], matrix_view[2][2]);
+ vec3 toEye = view_position - vWorldPos;
+ if (camera_params.w > 0.5) {
+ toEye = back * dot(toEye, back);
+ }
+ gl_Position = matrix_viewProjection * vec4(vWorldPos + toEye * uDepthOffset, 1.0);
+ vClipPos = gl_Position;
+ vToEye = toEye;
+ vNormal = cross(u, v);
+ }
+`;
+
+export const fragmentGLSL = /* glsl */`
+ #include "gammaPS"
+
+ uniform vec3 view_position;
+ uniform vec4 camera_params;
+
+ uniform vec2 uFade;
+ uniform float uSpacing;
+ uniform float uSubdivisions;
+ uniform vec3 uThickness;
+ uniform float uPixelRatio;
+ uniform float uShowAxes;
+ uniform vec4 uColorU;
+ uniform vec4 uColorV;
+ uniform vec4 uMajorColor;
+ uniform vec4 uMinorColor;
+
+ #ifdef GRID_CONTRAST
+ uniform sampler2D uSceneColorMap;
+ #endif
+
+ #ifdef GRID_DEPTH_DITHER
+ uniform sampler2D blueNoiseTex32;
+ #endif
+
+ varying vec2 vGridPos;
+ varying vec3 vWorldPos;
+ varying vec4 vClipPos;
+ varying vec3 vToEye;
+ varying vec3 vNormal;
+
+ // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c, with lines in pixels
+ float pristineGrid(vec2 uv, vec2 uvDeriv, float pixels, vec2 fade) {
+ vec2 targetWidth = min(pixels * uvDeriv, vec2(min(pixels / (${LINE_FADE_CELL} * uPixelRatio), 0.5)));
+ vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
+ vec2 lineAA = uvDeriv * 1.5;
+ vec2 gridUV = 1.0 - abs(fract(uv) * 2.0 - 1.0);
+ 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 *= fade;
+ return mix(grid2.x, 1.0, grid2.y);
+ }
+
+ // drops the origin line when the axes draw it
+ vec2 offAxis(vec2 uv) {
+ return uShowAxes > 0.5 ? step(0.5, abs(uv)) : vec2(1.0);
+ }
+
+ void main(void) {
+ vec2 pos = vGridPos / uSpacing;
+ vec2 ddx = dFdx(pos);
+ vec2 ddy = dFdy(pos);
+ vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y)));
+ float epsilon = 1.0 / 255.0;
+
+ // the level fading in, by cell size on screen; one fixed level without subdivisions
+ float lod = uSubdivisions > 1.0 ?
+ log(${LEVEL_FADE_CELL} * uPixelRatio * max(uvDeriv.x, uvDeriv.y)) / log(uSubdivisions) :
+ -1.0;
+ float finest = ceil(lod);
+
+ // coarsest level first; the finest fades in and the coarser turn major
+ vec4 color = uMinorColor;
+ float line = 0.0;
+ for (int i = ${LEVELS_ABOVE}; i >= 0; i--) {
+ float k = finest + float(i);
+ float scale = pow(uSubdivisions, -k);
+ float c = k - lod;
+ float major = clamp(c - 1.0, 0.0, 1.0);
+ float l = pristineGrid(pos * scale, uvDeriv * scale, mix(uThickness.y, uThickness.x, major), offAxis(pos * scale));
+ l *= clamp(c, 0.0, 1.0);
+ if (l > epsilon) {
+ color = mix(uMinorColor, uMajorColor, major);
+ line = l;
+ break;
+ }
+ }
+
+ // axis lines, from the coarsest level
+ float topScale = pow(uSubdivisions, -(finest + ${LEVELS_ABOVE}.0));
+ vec2 top = pos * topScale;
+ vec2 topDeriv = uvDeriv * topScale;
+ float lineU = abs(top.y) < 0.5 ? pristineGrid(top, topDeriv, uThickness.z, vec2(0.0, 1.0)) : 0.0;
+ float lineV = abs(top.x) < 0.5 ? pristineGrid(top, topDeriv, uThickness.z, vec2(1.0, 0.0)) : 0.0;
+ float axis = max(lineU, lineV) * uShowAxes;
+ if (axis > epsilon && axis >= line) {
+ color = lineU >= lineV ? uColorU : uColorV;
+ line = axis;
+ }
+ #ifdef GRID_CONTRAST
+ else {
+ // black over light, white over dark
+ vec3 under = texture2DLod(uSceneColorMap, (vClipPos.xy / vClipPos.w) * 0.5 + 0.5, 0.0).rgb;
+ color.rgb = vec3(1.0 - smoothstep(0.4, 0.6, dot(under, vec3(0.2126, 0.7152, 0.0722))));
+ }
+ #endif
+
+ // distance fade
+ float fadeEnd = min(uFade.y, camera_params.y * ${FAR_FRACTION});
+ float fadeStart = min(uFade.x, fadeEnd);
+ float f = clamp((length(vWorldPos - view_position) - fadeStart) / max(fadeEnd - fadeStart, 1e-5), 0.0, 1.0);
+
+ // grazing angle fade
+ float grazing = 1.0 - pow(1.0 - abs(dot(normalize(vToEye), normalize(vNormal))), 3.0);
+ float coverage = line * (1.0 - f * f * (3.0 - 2.0 * f)) * grazing;
+ float alpha = coverage * color.a;
+ if (alpha <= epsilon) {
+ discard;
+ }
+
+ #ifdef GRID_DEPTH
+ // write depth where coverage beats the noise, or half
+ #ifdef GRID_DEPTH_DITHER
+ float threshold = texture2DLod(blueNoiseTex32, fract(gl_FragCoord.xy / 32.0), 0.0).y;
+ #else
+ float threshold = 0.5;
+ #endif
+ if (coverage < threshold) {
+ discard;
+ }
+ gl_FragColor = vec4(0.0);
+ #else
+ gl_FragColor = vec4(gammaCorrectOutput(decodeGamma(color.rgb)), alpha);
+ #endif
+ }
+`;
+
+export const vertexWGSL = /* wgsl */`
+ attribute vertex_position: vec3f;
+
+ uniform matrix_model: mat4x4f;
+ uniform matrix_view: mat4x4f;
+ uniform matrix_viewProjection: mat4x4f;
+ uniform view_position: vec3f;
+ uniform camera_params: vec4f;
+
+ uniform uAxisU: vec3f;
+ uniform uAxisV: vec3f;
+ uniform uInfinite: f32;
+ uniform uFade: vec2f;
+ uniform uDepthOffset: f32;
+
+ varying vGridPos: vec2f;
+ varying vWorldPos: vec3f;
+ varying vClipPos: vec4f;
+ varying vToEye: vec3f;
+ varying vNormal: vec3f;
+
+ @vertex
+ fn vertexMain(input: VertexInput) -> VertexOutput {
+ var output: VertexOutput;
+
+ // the node's scale sizes a fixed grid, never its cells
+ let model = uniform.matrix_model;
+ let origin = model[3].xyz;
+ let axisU = (model * vec4f(uniform.uAxisU, 0.0)).xyz;
+ let axisV = (model * vec4f(uniform.uAxisV, 0.0)).xyz;
+ let u = normalize(axisU);
+ let v = normalize(axisV);
+
+ // an infinite grid follows the camera and spans the fade distance
+ let reach = 2.0 * min(uniform.uFade.y, uniform.camera_params.y * ${FAR_FRACTION});
+ let center = vec2f(dot(uniform.view_position - origin, u), dot(uniform.view_position - origin, v)) * uniform.uInfinite;
+ let gridPos = center + input.vertex_position.xz * mix(vec2f(length(axisU), length(axisV)), vec2f(reach), uniform.uInfinite);
+ let worldPos = origin + u * gridPos.x + v * gridPos.y;
+ output.vGridPos = gridPos;
+ output.vWorldPos = worldPos;
+
+ // offset along the view ray, so only depth changes
+ let view = uniform.matrix_view;
+ let back = vec3f(view[0][2], view[1][2], view[2][2]);
+ var toEye = uniform.view_position - worldPos;
+ if (uniform.camera_params.w > 0.5) {
+ toEye = back * dot(toEye, back);
+ }
+ output.position = uniform.matrix_viewProjection * vec4f(worldPos + toEye * uniform.uDepthOffset, 1.0);
+ output.vClipPos = output.position;
+ output.vToEye = toEye;
+ output.vNormal = cross(u, v);
+ return output;
+ }
+`;
+
+export const fragmentWGSL = /* wgsl */`
+ #include "gammaPS"
+
+ uniform view_position: vec3f;
+ uniform camera_params: vec4f;
+
+ uniform uFade: vec2f;
+ uniform uSpacing: f32;
+ uniform uSubdivisions: f32;
+ uniform uThickness: vec3f;
+ uniform uPixelRatio: f32;
+ uniform uShowAxes: f32;
+ uniform uColorU: vec4f;
+ uniform uColorV: vec4f;
+ uniform uMajorColor: vec4f;
+ uniform uMinorColor: vec4f;
+
+ #ifdef GRID_CONTRAST
+ var uSceneColorMap: texture_2d;
+ var uSceneColorMapSampler: sampler;
+ #endif
+
+ #ifdef GRID_DEPTH_DITHER
+ var blueNoiseTex32: texture_2d;
+ var blueNoiseTex32Sampler: sampler;
+ #endif
+
+ varying vGridPos: vec2f;
+ varying vWorldPos: vec3f;
+ varying vClipPos: vec4f;
+ varying vToEye: vec3f;
+ varying vNormal: vec3f;
+
+ // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c, with lines in pixels
+ fn pristineGrid(uv: vec2f, uvDeriv: vec2f, pixels: f32, fade: vec2f) -> f32 {
+ let targetWidth = min(pixels * uvDeriv, vec2f(min(pixels / (${LINE_FADE_CELL} * uniform.uPixelRatio), 0.5)));
+ let drawWidth = clamp(targetWidth, uvDeriv, vec2f(0.5));
+ let lineAA = uvDeriv * 1.5;
+ let gridUV = 1.0 - abs(fract(uv) * 2.0 - 1.0);
+ var grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
+ grid2 *= clamp(targetWidth / drawWidth, vec2f(0.0), vec2f(1.0));
+ grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, vec2f(0.0), vec2f(1.0)));
+ grid2 *= fade;
+ return mix(grid2.x, 1.0, grid2.y);
+ }
+
+ // drops the origin line when the axes draw it
+ fn offAxis(uv: vec2f) -> vec2f {
+ return select(vec2f(1.0), step(vec2f(0.5), abs(uv)), uniform.uShowAxes > 0.5);
+ }
+
+ @fragment
+ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
+ var output: FragmentOutput;
+
+ // derivatives need uniform control flow
+ let pos = input.vGridPos / uniform.uSpacing;
+ let ddx = dpdx(pos);
+ let ddy = dpdy(pos);
+ let uvDeriv = vec2f(length(vec2f(ddx.x, ddy.x)), length(vec2f(ddx.y, ddy.y)));
+ let epsilon = 1.0 / 255.0;
+
+ // the level fading in, by cell size on screen; one fixed level without subdivisions
+ let lod = select(
+ -1.0,
+ log(${LEVEL_FADE_CELL} * uniform.uPixelRatio * max(uvDeriv.x, uvDeriv.y)) / log(uniform.uSubdivisions),
+ uniform.uSubdivisions > 1.0
+ );
+ let finest = ceil(lod);
+
+ // coarsest level first; the finest fades in and the coarser turn major
+ var color = uniform.uMinorColor;
+ var line = 0.0;
+ for (var i: i32 = ${LEVELS_ABOVE}; i >= 0; i--) {
+ let k = finest + f32(i);
+ let scale = pow(uniform.uSubdivisions, -k);
+ let c = k - lod;
+ let major = clamp(c - 1.0, 0.0, 1.0);
+ let thickness = mix(uniform.uThickness.y, uniform.uThickness.x, major);
+ let l = pristineGrid(pos * scale, uvDeriv * scale, thickness, offAxis(pos * scale)) * clamp(c, 0.0, 1.0);
+ if (l > epsilon) {
+ color = mix(uniform.uMinorColor, uniform.uMajorColor, major);
+ line = l;
+ break;
+ }
+ }
+
+ // axis lines, from the coarsest level
+ let topScale = pow(uniform.uSubdivisions, -(finest + ${LEVELS_ABOVE}.0));
+ let top = pos * topScale;
+ let topDeriv = uvDeriv * topScale;
+ let lineU = select(0.0, pristineGrid(top, topDeriv, uniform.uThickness.z, vec2f(0.0, 1.0)), abs(top.y) < 0.5);
+ let lineV = select(0.0, pristineGrid(top, topDeriv, uniform.uThickness.z, vec2f(1.0, 0.0)), abs(top.x) < 0.5);
+ let axis = max(lineU, lineV) * uniform.uShowAxes;
+ if (axis > epsilon && axis >= line) {
+ color = select(uniform.uColorV, uniform.uColorU, lineU >= lineV);
+ line = axis;
+ }
+ #ifdef GRID_CONTRAST
+ else {
+ // black over light, white over dark
+ var uv = (input.vClipPos.xy / input.vClipPos.w) * 0.5 + vec2f(0.5);
+ uv.y = 1.0 - uv.y;
+ let under = textureSampleLevel(uSceneColorMap, uSceneColorMapSampler, uv, 0.0).rgb;
+ color = vec4f(vec3f(1.0 - smoothstep(0.4, 0.6, dot(under, vec3f(0.2126, 0.7152, 0.0722)))), color.a);
+ }
+ #endif
+
+ // distance fade
+ let fadeEnd = min(uniform.uFade.y, uniform.camera_params.y * ${FAR_FRACTION});
+ let fadeStart = min(uniform.uFade.x, fadeEnd);
+ let f = clamp((length(input.vWorldPos - uniform.view_position) - fadeStart) / max(fadeEnd - fadeStart, 1e-5), 0.0, 1.0);
+
+ // grazing angle fade
+ let grazing = 1.0 - pow(1.0 - abs(dot(normalize(input.vToEye), normalize(input.vNormal))), 3.0);
+ let coverage = line * (1.0 - f * f * (3.0 - 2.0 * f)) * grazing;
+ let alpha = coverage * color.a;
+ if (alpha <= epsilon) {
+ discard;
+ }
+
+ #ifdef GRID_DEPTH
+ // write depth where coverage beats the noise, or half
+ #ifdef GRID_DEPTH_DITHER
+ let threshold = textureSampleLevel(blueNoiseTex32, blueNoiseTex32Sampler, fract(pcPosition.xy / 32.0), 0.0).y;
+ #else
+ let threshold = 0.5;
+ #endif
+ if (coverage < threshold) {
+ discard;
+ }
+ output.color = vec4f(0.0);
+ #else
+ output.color = vec4f(gammaCorrectOutput(decodeGamma3(color.rgb)), alpha);
+ #endif
+ return output;
+ }
+`;
diff --git a/src/extras/index.js b/src/extras/index.js
index 284f2a7f5b4..4c0db9e1013 100644
--- a/src/extras/index.js
+++ b/src/extras/index.js
@@ -21,6 +21,9 @@ export {
export { LINEWIDTH_SCREEN, LINEWIDTH_WORLD, WideLineRenderer } from './renderers/wide-line-renderer.js';
export { WireRenderer } from './renderers/wire-renderer.js';
+// GRID
+export { Grid } from './grid/grid.js';
+
// EXPORTERS
export { UsdzExporter } from './exporters/usdz-exporter.js';
export { GltfExporter } from './exporters/gltf-exporter.js';