Spaces:
Running
Running
<!-- Credits to Fyrestar for the https://github.com/Fyrestar/THREE.InfiniteGridHelper --> | |
<script lang="ts"> | |
import { T, useTask, useThrelte } from "@threlte/core"; | |
import { Color, DoubleSide, Plane, Vector3, Mesh } from "three"; | |
import * as THREE from "three"; | |
// Grid shader code with improved precision and stability | |
import { revision } from "@threlte/core"; | |
// Props | |
let { | |
cellColor = "#71717A", | |
sectionColor = "#707070", | |
cellSize = 1, | |
backgroundColor = "#000000", | |
backgroundOpacity = 0, | |
sectionSize = 10, | |
plane = "xz", | |
gridSize = [20, 20], | |
followCamera = false, | |
infiniteGrid = false, | |
fadeDistance = 100, | |
fadeStrength = 1, | |
fadeOrigin = undefined, | |
cellThickness = 1, | |
sectionThickness = 2, | |
side = DoubleSide, | |
type = "grid", | |
axis = "x", | |
maxRadius = 0, | |
cellDividers = 6, | |
sectionDividers = 2, | |
floorColor = "#2a2a2a", | |
floorOpacity = 0.3, | |
ref = $bindable(), | |
children = undefined, | |
...props | |
} = $props(); | |
// Shared fade calculation function for both shaders | |
const fadeCalculation = /*glsl*/ ` | |
float calculateFade(vec3 worldPos, float viewZ, vec3 fadeOrigin, float fadeDistance, float fadeStrength, float cameraNear, float cameraFar) { | |
float dist = distance(fadeOrigin, worldPos); | |
float fadeFactor = 1.0 - clamp(dist / fadeDistance, 0.0, 1.0); | |
fadeFactor = pow(fadeFactor, fadeStrength); | |
float viewDepthFade = 1.0 - clamp((viewZ - cameraNear) / (cameraFar - cameraNear), 0.0, 1.0); | |
viewDepthFade = smoothstep(0.0, 0.3, viewDepthFade); | |
return min(fadeFactor, viewDepthFade); | |
} | |
`; | |
const vertexShader = /*glsl*/ ` | |
varying vec3 localPosition; | |
varying vec4 worldPosition; | |
varying float vViewZ; | |
uniform vec3 worldCamProjPosition; | |
uniform vec3 worldPlanePosition; | |
uniform float fadeDistance; | |
uniform bool infiniteGrid; | |
uniform bool followCamera; | |
uniform int coord0, coord1, coord2; | |
void main() { | |
localPosition = vec3(position[coord0], position[coord1], position[coord2]); | |
if (infiniteGrid) localPosition *= 1.0 + fadeDistance; | |
worldPosition = modelMatrix * vec4(localPosition, 1.0); | |
if (followCamera) { | |
worldPosition.xyz += (worldCamProjPosition - worldPlanePosition); | |
localPosition = (inverse(modelMatrix) * worldPosition).xyz; | |
} | |
vec4 mvPosition = viewMatrix * worldPosition; | |
vViewZ = -mvPosition.z; | |
gl_Position = projectionMatrix * mvPosition; | |
} | |
`; | |
const fragmentShader = /*glsl*/ ` | |
#define PI 3.141592653589793 | |
varying vec3 localPosition; | |
varying vec4 worldPosition; | |
varying float vViewZ; | |
uniform float cellSize, sectionSize, cellThickness, sectionThickness; | |
uniform vec3 cellColor, sectionColor, backgroundColor, fadeOrigin; | |
uniform float backgroundOpacity, fadeDistance, fadeStrength, cameraNear, cameraFar; | |
uniform bool infiniteGrid; | |
uniform int coord0, coord1, coord2, gridType, lineGridCoord; | |
uniform float circleGridMaxRadius, polarCellDividers, polarSectionDividers; | |
${fadeCalculation} | |
float getSquareGrid(float size, float thickness, vec3 localPos) { | |
vec2 coord = localPos.xy / size; | |
vec2 derivative = fwidth(coord); | |
vec2 grid = abs(fract(coord - 0.5) - 0.5) / derivative; | |
float line = min(grid.x, grid.y) + 1.0 - thickness; | |
return clamp(1.0 - line, 0.0, 1.0); | |
} | |
float getLinesGrid(float size, float thickness, vec3 localPos) { | |
float coord = localPos[lineGridCoord] / size; | |
float derivative = fwidth(coord); | |
float line = abs(fract(coord - 0.5) - 0.5) / derivative - thickness * 0.5; | |
return clamp(1.0 - line, 0.0, 1.0); | |
} | |
float getCirclesGrid(float size, float thickness, vec3 localPos) { | |
float coord = length(localPos.xy) / size; | |
float derivative = fwidth(coord); | |
float line = abs(fract(coord - 0.5) - 0.5) / derivative - thickness * 0.5; | |
if (!infiniteGrid && circleGridMaxRadius > 0.0 && coord > circleGridMaxRadius + thickness * 0.1) discard; | |
return clamp(1.0 - line, 0.0, 1.0); | |
} | |
float getPolarGrid(float size, float thickness, float polarDividers, vec3 localPos) { | |
float rad = length(localPos.xy) / size; | |
vec2 coord = vec2(rad, atan(localPos.x, localPos.y) * polarDividers / PI); | |
vec2 derivative = fwidth(coord); | |
vec2 grid = abs(fract(coord - 0.5) - 0.5) / derivative; | |
float line = min(grid.x, grid.y) + 1.0 - thickness; | |
if (!infiniteGrid && circleGridMaxRadius > 0.0 && rad > circleGridMaxRadius + thickness * 0.1) discard; | |
return clamp(1.0 - line, 0.0, 1.0); | |
} | |
void main() { | |
float g1 = 0.0, g2 = 0.0; | |
vec3 localPos = vec3(localPosition[coord0], localPosition[coord1], localPosition[coord2]); | |
if (gridType == 0) { | |
g1 = getSquareGrid(cellSize, cellThickness, localPos); | |
g2 = getSquareGrid(sectionSize, sectionThickness, localPos); | |
} else if (gridType == 1) { | |
g1 = getLinesGrid(cellSize, cellThickness, localPos); | |
g2 = getLinesGrid(sectionSize, sectionThickness, localPos); | |
} else if (gridType == 2) { | |
g1 = getCirclesGrid(cellSize, cellThickness, localPos); | |
g2 = getCirclesGrid(sectionSize, sectionThickness, localPos); | |
} else if (gridType == 3) { | |
g1 = getPolarGrid(cellSize, cellThickness, polarCellDividers, localPos); | |
g2 = getPolarGrid(sectionSize, sectionThickness, polarSectionDividers, localPos); | |
} | |
float fadeFactor = calculateFade(worldPosition.xyz, vViewZ, fadeOrigin, fadeDistance, fadeStrength, cameraNear, cameraFar); | |
vec3 color = mix(cellColor, sectionColor, clamp(sectionThickness * g2, 0.0, 1.0)); | |
float gridAlpha = clamp((g1 + g2) * fadeFactor, 0.0, 1.0); | |
if (backgroundOpacity > 0.0) { | |
vec3 finalColor = mix(backgroundColor, color, gridAlpha); | |
float blendedAlpha = clamp(max(gridAlpha, backgroundOpacity * fadeFactor), 0.0, 1.0); | |
gl_FragColor = vec4(finalColor, blendedAlpha); | |
} else { | |
gl_FragColor = vec4(color, gridAlpha); | |
} | |
if (gl_FragColor.a < 0.05) discard; | |
#include <tonemapping_fragment> | |
#include <${revision < 154 ? "encodings_fragment" : "colorspace_fragment"}> | |
} | |
`; | |
// Simple floor shader | |
const floorVertexShader = /*glsl*/ ` | |
varying vec3 vWorldPosition; | |
varying float vViewZ; | |
void main() { | |
vec4 worldPosition = modelMatrix * vec4(position, 1.0); | |
vWorldPosition = worldPosition.xyz; | |
vec4 mvPosition = viewMatrix * worldPosition; | |
vViewZ = -mvPosition.z; | |
gl_Position = projectionMatrix * mvPosition; | |
} | |
`; | |
const floorFragmentShader = /*glsl*/ ` | |
uniform vec3 floorColor, fadeOrigin; | |
uniform float floorOpacity, fadeDistance, fadeStrength, cameraNear, cameraFar; | |
varying vec3 vWorldPosition; | |
varying float vViewZ; | |
${fadeCalculation} | |
void main() { | |
float fadeFactor = calculateFade(vWorldPosition, vViewZ, fadeOrigin, fadeDistance, fadeStrength, cameraNear, cameraFar); | |
float finalOpacity = floorOpacity * fadeFactor; | |
gl_FragColor = vec4(floorColor, finalOpacity); | |
if (gl_FragColor.a < 0.01) discard; | |
} | |
`; | |
const mesh = new Mesh(); | |
const { invalidate, camera } = useThrelte(); | |
const gridPlane = new Plane(); | |
const upVector = new Vector3(0, 1, 0); | |
const zeroVector = new Vector3(0, 0, 0); | |
const axisToInt: Record<string, number> = { x: 0, y: 1, z: 2 }; | |
const planeToAxes: Record<string, string> = { xz: "xzy", xy: "xyz", zy: "zyx" }; | |
const gridType = { grid: 0, lines: 1, circular: 2, polar: 3 }; | |
// Shared uniforms (used by both grid and floor) | |
const sharedUniforms = { | |
fadeOrigin: { value: new Vector3() }, | |
fadeDistance: { value: fadeDistance }, | |
fadeStrength: { value: fadeStrength }, | |
cameraNear: { value: 0.1 }, | |
cameraFar: { value: 1000 } | |
}; | |
// Grid uniforms | |
const uniforms = { | |
...sharedUniforms, | |
cellSize: { value: cellSize }, | |
sectionSize: { value: sectionSize }, | |
cellColor: { value: new Color(cellColor) }, | |
sectionColor: { value: new Color(sectionColor) }, | |
backgroundColor: { value: new Color(backgroundColor) }, | |
backgroundOpacity: { value: backgroundOpacity }, | |
cellThickness: { value: cellThickness }, | |
sectionThickness: { value: sectionThickness }, | |
infiniteGrid: { value: infiniteGrid }, | |
followCamera: { value: followCamera }, | |
coord0: { value: 0 }, | |
coord1: { value: 2 }, | |
coord2: { value: 1 }, | |
gridType: { value: gridType.grid }, | |
lineGridCoord: { value: axisToInt[axis as keyof typeof axisToInt] || 0 }, | |
circleGridMaxRadius: { value: maxRadius }, | |
polarCellDividers: { value: cellDividers }, | |
polarSectionDividers: { value: sectionDividers }, | |
worldCamProjPosition: { value: new Vector3() }, | |
worldPlanePosition: { value: new Vector3() } | |
}; | |
// Floor uniforms (simpler, reusing shared uniforms) | |
const floorUniforms = { | |
...sharedUniforms, | |
floorColor: { value: new Color(floorColor) }, | |
floorOpacity: { value: floorOpacity } | |
}; | |
// Single update effect for all uniforms | |
$effect.pre(() => { | |
const axes = planeToAxes[plane] || "xzy"; | |
const [c0, c1, c2] = [axes.charAt(0), axes.charAt(1), axes.charAt(2)].map( | |
(c) => axisToInt[c as keyof typeof axisToInt] | |
); | |
// Update grid uniforms | |
Object.assign(uniforms, { | |
coord0: { value: c0 }, | |
coord1: { value: c1 }, | |
coord2: { value: c2 }, | |
cellSize: { value: cellSize }, | |
sectionSize: { value: sectionSize }, | |
cellThickness: { value: cellThickness }, | |
sectionThickness: { value: sectionThickness }, | |
backgroundOpacity: { value: backgroundOpacity }, | |
infiniteGrid: { value: infiniteGrid }, | |
followCamera: { value: followCamera } | |
}); | |
uniforms.cellColor.value.set(cellColor); | |
uniforms.sectionColor.value.set(sectionColor); | |
uniforms.backgroundColor.value.set(backgroundColor); | |
// Update shared uniforms (affects both grid and floor) | |
sharedUniforms.fadeDistance.value = fadeDistance; | |
sharedUniforms.fadeStrength.value = fadeStrength; | |
floorUniforms.floorColor.value.set(floorColor); | |
floorUniforms.floorOpacity.value = floorOpacity; | |
// Update camera uniforms | |
if (camera.current && "near" in camera.current && "far" in camera.current) { | |
const cam = camera.current as THREE.PerspectiveCamera; | |
sharedUniforms.cameraNear.value = cam.near; | |
sharedUniforms.cameraFar.value = cam.far; | |
} | |
// Update grid type | |
const typeMap = { grid: 0, lines: 1, circular: 2, polar: 3 }; | |
uniforms.gridType.value = typeMap[type as keyof typeof typeMap] || 0; | |
if (type === "lines") | |
uniforms.lineGridCoord.value = axisToInt[axis as keyof typeof axisToInt] || 0; | |
if (type === "circular" || type === "polar") { | |
uniforms.circleGridMaxRadius.value = maxRadius; | |
if (type === "polar") { | |
uniforms.polarCellDividers.value = cellDividers; | |
uniforms.polarSectionDividers.value = sectionDividers; | |
} | |
} | |
invalidate(); | |
}); | |
// Single task for both grid and floor fade origins | |
useTask( | |
() => { | |
gridPlane.setFromNormalAndCoplanarPoint(upVector, zeroVector).applyMatrix4(mesh.matrixWorld); | |
const material = mesh.material as THREE.ShaderMaterial; | |
if (material?.uniforms) { | |
const { worldCamProjPosition, worldPlanePosition } = material.uniforms; | |
const projectedPoint = gridPlane.projectPoint( | |
camera.current.position, | |
worldCamProjPosition.value | |
); | |
if (!fadeOrigin) sharedUniforms.fadeOrigin.value = projectedPoint; | |
worldPlanePosition.value.set(0, 0, 0).applyMatrix4(mesh.matrixWorld); | |
} | |
}, | |
{ autoInvalidate: false } | |
); | |
</script> | |
<!-- Shadow-receiving floor underneath --> | |
<T.Mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow position.y={0} {...props}> | |
<T.PlaneGeometry | |
args={infiniteGrid | |
? [1000, 1000] | |
: typeof gridSize == "number" | |
? [gridSize, gridSize] | |
: gridSize} | |
/> | |
<T.ShadowMaterial | |
transparent={true} | |
opacity={0.3} | |
polygonOffset={true} | |
polygonOffsetFactor={1} | |
polygonOffsetUnits={1} | |
/> | |
</T.Mesh> | |
<!-- Fading floor --> | |
<T.Mesh rotation={[-Math.PI / 2, 0, 0]} position.y={0} {...props}> | |
<T.PlaneGeometry | |
args={infiniteGrid | |
? [1000, 1000] | |
: typeof gridSize == "number" | |
? [gridSize, gridSize] | |
: gridSize} | |
/> | |
<T.ShaderMaterial | |
vertexShader={floorVertexShader} | |
fragmentShader={floorFragmentShader} | |
uniforms={floorUniforms} | |
transparent={true} | |
side={THREE.DoubleSide} | |
depthTest={true} | |
depthWrite={false} | |
polygonOffset={true} | |
polygonOffsetFactor={-1} | |
polygonOffsetUnits={-1} | |
/> | |
</T.Mesh> | |
<!-- Grid lines --> | |
<T is={mesh} bind:ref frustumCulled={false} position.y={0.005} {...props}> | |
<T.ShaderMaterial | |
{fragmentShader} | |
{vertexShader} | |
{uniforms} | |
transparent | |
{side} | |
depthTest={true} | |
depthWrite={false} | |
/> | |
{#if children} | |
{@render children({ ref: mesh })} | |
{:else} | |
<T.PlaneGeometry args={typeof gridSize == "number" ? [gridSize, gridSize] : gridSize} /> | |
{/if} | |
</T> | |