playcanvas
Version:
PlayCanvas WebGL game engine
450 lines (379 loc) • 12.4 kB
JavaScript
import {
ShaderMaterial,
SEMANTIC_POSITION,
SEMANTIC_TEXCOORD0,
BLEND_NORMAL,
CULLFACE_NONE,
PlaneGeometry,
Mesh,
MeshInstance,
Color,
Script,
Vec2
} from 'playcanvas';
const tmpVa = new Vec2();
const EPISILON = 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;
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<bool>(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);
}
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;
}
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;
}
discard;
return output;
}
`;
class Grid extends Script {
static scriptName = 'grid';
/**
* @type {number}
*/
static RESOLUTION_LOW = 0;
/**
* @type {number}
*/
static RESOLUTION_MEDIUM = 1;
/**
* @type {number}
*/
static RESOLUTION_HIGH = 2;
/**
* @type {ShaderMaterial}
* @private
*/
_material;
/**
* @type {MeshInstance}
* @private
*/
_meshInstance;
/**
* @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;
initialize() {
// check if the entity already has a render component
if (this.entity.render) {
console.error('The entity already has a render component.');
return;
}
// create render component
this.entity.addComponent('render', {
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) > EPISILON) {
this._set('uHalfExtents', halfExtents);
}
});
// enable/disable the mesh instance
this.on('enable', () => {
this._meshInstance.visible = true;
});
this.on('disable', () => {
this._meshInstance.visible = false;
});
this.on('destroy', this.destroy, this);
}
/**
* @param {Vec2} vec - The vector to copy the half extents to.
* @returns {Vec2} - The half extents.
* @private
*/
_calcHalfExtents(vec) {
const scale = this.entity.getLocalScale();
return vec.set(scale.x / 2, scale.z / 2);
}
/**
* @param {string} name - The name of the parameter.
* @param {Color|Vec2|number} value - The value of the parameter.
* @private
*/
_set(name, value) {
if (!this._material) {
return;
}
if (value instanceof Color) {
this._material.setParameter(name, [value.r, value.g, value.b]);
}
if (value instanceof Vec2) {
this._material.setParameter(name, [value.x, value.y]);
}
if (typeof value === 'number') {
this._material.setParameter(name, value);
}
this._material.update();
this._meshInstance.material = this._material;
}
/**
* @attribute
* @title Grid Color X
* @description The color of the grid lines along the X axis.
* @type {Color}
* @default [1, 0.3, 0.3, 1]
*/
set colorX(value) {
if (!(value instanceof Color)) {
return;
}
this._colorX.copy(value);
this._set('uColorX', this._colorX);
}
get colorX() {
return this._colorX;
}
/**
* @attribute
* @title Grid Color Z
* @description The color of the grid lines along the Z axis.
* @type {Color}
* @default [0.3, 0.3, 1, 1]
*/
set colorZ(value) {
if (!(value instanceof Color)) {
return;
}
this._colorZ.copy(value);
this._set('uColorZ', this._colorZ);
}
get colorZ() {
return this._colorZ;
}
/**
* @attribute
* @title Grid Resolution
* @description The resolution of the grid.
* @type {number}
* @default 2
*/
set resolution(value) {
this._resolution = value;
this._set('uResolution', this._resolution);
}
get resolution() {
return this._resolution;
}
destroy() {
this.entity.removeComponent('render');
}
}
export { Grid };