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> | |