159 lines
3.8 KiB
TypeScript
159 lines
3.8 KiB
TypeScript
import {
|
|
DataArrayTexture,
|
|
LinearFilter,
|
|
RGBAFormat,
|
|
SRGBColorSpace,
|
|
Texture,
|
|
Vector4,
|
|
} from "three";
|
|
import {
|
|
Break,
|
|
Fn,
|
|
If,
|
|
Loop,
|
|
oneMinus,
|
|
positionWorld,
|
|
texture,
|
|
uniform,
|
|
uniformArray,
|
|
vec4,
|
|
} from "three/tsl";
|
|
import { MeshStandardNodeMaterial } from "three/webgpu";
|
|
|
|
export interface TileData {
|
|
xmin: number;
|
|
ymin: number;
|
|
xmax: number;
|
|
ymax: number;
|
|
x: number;
|
|
y: number;
|
|
zoom: number;
|
|
texture: Texture;
|
|
}
|
|
|
|
const maxTiles = 48;
|
|
const width = 256;
|
|
const height = 256;
|
|
const size = width * height;
|
|
|
|
const canvas = new OffscreenCanvas(width, height);
|
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
|
|
const tileBounds: Vector4[] = Array(maxTiles).fill(new Vector4(0, 0, 0, 0));
|
|
|
|
const data = new Uint8Array(4 * size * maxTiles);
|
|
const tileCache: {
|
|
[key: string]: {
|
|
imageData: Uint8ClampedArray;
|
|
};
|
|
} = {};
|
|
|
|
const dataArrayTexture = new DataArrayTexture(data, width, height, maxTiles);
|
|
dataArrayTexture.format = RGBAFormat;
|
|
dataArrayTexture.generateMipmaps = false;
|
|
dataArrayTexture.magFilter = LinearFilter;
|
|
dataArrayTexture.minFilter = LinearFilter;
|
|
dataArrayTexture.colorSpace = SRGBColorSpace;
|
|
dataArrayTexture.needsUpdate = true;
|
|
|
|
// Create shader material
|
|
export const topoNodeMaterial = new MeshStandardNodeMaterial({
|
|
alphaToCoverage: true,
|
|
});
|
|
const tileBoundsUniform = uniformArray(tileBounds);
|
|
const dataArrayTextureUniform = uniform(dataArrayTexture);
|
|
|
|
const fragmentShader = /*#__PURE__*/ Fn(() => {
|
|
const color = vec4(191.0 / 255.0, 209.0 / 255.0, 229.0 / 255.0, 1.0).toVar();
|
|
Loop({ start: 0, end: maxTiles, condition: "<" }, ({ i }) => {
|
|
const bounds = tileBoundsUniform.element(i);
|
|
|
|
If(
|
|
positionWorld.x
|
|
.greaterThanEqual(bounds.x)
|
|
.and(positionWorld.x.lessThanEqual(bounds.y))
|
|
.and(positionWorld.y.greaterThanEqual(bounds.z))
|
|
.and(positionWorld.y.lessThanEqual(bounds.w)),
|
|
() => {
|
|
let uv = positionWorld.xy
|
|
.sub(bounds.xz)
|
|
.div(bounds.yw.sub(bounds.xz))
|
|
.toVar();
|
|
uv.y.assign(oneMinus(uv.y));
|
|
|
|
const tile = texture(dataArrayTextureUniform.value, uv);
|
|
color.assign(tile.depth(i));
|
|
Break();
|
|
}
|
|
);
|
|
});
|
|
|
|
return color;
|
|
});
|
|
|
|
topoNodeMaterial.colorNode = fragmentShader();
|
|
|
|
let oldKeys: string[] = [];
|
|
export function updateTiles(newTiles: TileData[]) {
|
|
if (newTiles.length > maxTiles) {
|
|
newTiles = newTiles.slice(0, maxTiles);
|
|
}
|
|
|
|
const newKeys = newTiles.map((t) => getTileDataKey(t));
|
|
const update =
|
|
oldKeys.some((k, i) => k !== newKeys[i]) || oldKeys.length === 0;
|
|
|
|
// Only update if tiles changed
|
|
if (update) {
|
|
for (let i = 0; i < newTiles.length; i++) {
|
|
updateDataArrayTexture(newTiles[i], i);
|
|
}
|
|
|
|
dataArrayTexture.needsUpdate = true;
|
|
oldKeys = newKeys;
|
|
}
|
|
}
|
|
|
|
// Update buffer
|
|
function updateDataArrayTexture(tileData: TileData, index: number) {
|
|
const k = getTileDataKey(tileData);
|
|
const cachedData = tileCache[k]?.imageData;
|
|
|
|
if (cachedData) {
|
|
tileBounds[index] = getTileBounds(tileData);
|
|
data.set(cachedData, index * size * 4);
|
|
} else {
|
|
const imageData = getImageData(tileData.texture);
|
|
|
|
if (imageData) {
|
|
// Update cache and buffer
|
|
tileCache[k] = { imageData };
|
|
tileBounds[index] = getTileBounds(tileData);
|
|
data.set(imageData, index * size * 4);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getTileDataKey(t: TileData) {
|
|
return `${t.zoom}/${t.x}/${t.y}`;
|
|
}
|
|
|
|
function getTileBounds(t: TileData) {
|
|
return new Vector4(t.xmin, t.xmax, t.ymin, t.ymax);
|
|
}
|
|
|
|
// Create a canvas and draw the image on it
|
|
function getImageData(texture: Texture) {
|
|
const image = texture.source.data;
|
|
|
|
// Draw the image onto the canvas
|
|
if (ctx) {
|
|
ctx.drawImage(image, 0, 0);
|
|
|
|
// Get the pixel data from the canvas
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
return imageData.data;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|