import { rotateScreenPoint, type ScreenPoint } from '../camera/camera' import { type TileStore, type TileStoreEntry } from '../tile/tileStore' import { TileLayer } from '../layer/tileLayer' import { buildCamera, type MapScene } from './mapRenderer' interface TextureRecord { key: string texture: any } function createShader(gl: any, type: number, source: string): any { const shader = gl.createShader(type) if (!shader) { throw new Error('WebGL shader 创建失败') } gl.shaderSource(shader, source) gl.compileShader(shader) if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const message = gl.getShaderInfoLog(shader) || 'unknown shader error' gl.deleteShader(shader) throw new Error(message) } return shader } function createProgram(gl: any, vertexSource: string, fragmentSource: string): any { const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource) const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource) const program = gl.createProgram() if (!program) { throw new Error('WebGL program 创建失败') } gl.attachShader(program, vertexShader) gl.attachShader(program, fragmentShader) gl.linkProgram(program) gl.deleteShader(vertexShader) gl.deleteShader(fragmentShader) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { const message = gl.getProgramInfoLog(program) || 'unknown program error' gl.deleteProgram(program) throw new Error(message) } return program } export class WebGLTileRenderer { canvas: any gl: any tileLayer: TileLayer tileStore: TileStore dpr: number program: any positionBuffer: any texCoordBuffer: any positionLocation: number texCoordLocation: number textureCache: Map constructor(tileLayer: TileLayer, tileStore: TileStore) { this.canvas = null this.gl = null this.tileLayer = tileLayer this.tileStore = tileStore this.dpr = 1 this.program = null this.positionBuffer = null this.texCoordBuffer = null this.positionLocation = -1 this.texCoordLocation = -1 this.textureCache = new Map() } attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void { this.canvas = canvasNode this.dpr = dpr || 1 canvasNode.width = Math.max(1, Math.floor(width * this.dpr)) canvasNode.height = Math.max(1, Math.floor(height * this.dpr)) const gl = canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl') if (!gl) { throw new Error('当前环境不支持 WebGL') } this.gl = gl this.program = createProgram( gl, 'attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord; }', 'precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); }', ) this.positionBuffer = gl.createBuffer() this.texCoordBuffer = gl.createBuffer() this.positionLocation = gl.getAttribLocation(this.program, 'a_position') this.texCoordLocation = gl.getAttribLocation(this.program, 'a_texCoord') gl.viewport(0, 0, canvasNode.width, canvasNode.height) gl.disable(gl.DEPTH_TEST) gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) this.tileStore.attachCanvas(canvasNode) } destroy(): void { if (this.gl) { this.textureCache.forEach((record) => { this.gl && this.gl.deleteTexture(record.texture) }) if (this.program) { this.gl.deleteProgram(this.program) } if (this.positionBuffer) { this.gl.deleteBuffer(this.positionBuffer) } if (this.texCoordBuffer) { this.gl.deleteBuffer(this.texCoordBuffer) } } this.textureCache.clear() this.program = null this.positionBuffer = null this.texCoordBuffer = null this.gl = null this.canvas = null } render(scene: MapScene): void { if (!this.gl || !this.program || !this.positionBuffer || !this.texCoordBuffer) { return } const gl = this.gl const camera = buildCamera(scene) const tiles = this.tileLayer.prepareTiles(scene, camera, this.tileStore) gl.viewport(0, 0, this.canvas.width, this.canvas.height) gl.clearColor(0.8588, 0.9333, 0.8314, 1) gl.clear(gl.COLOR_BUFFER_BIT) gl.useProgram(this.program) for (const tile of tiles) { const readyEntry = this.tileStore.getEntry(tile.url) if (readyEntry && readyEntry.status === 'ready' && readyEntry.image) { this.drawEntry(readyEntry, tile.url, 0, 0, readyEntry.image.width || 256, readyEntry.image.height || 256, tile.leftPx, tile.topPx, tile.sizePx, tile.sizePx, scene) this.tileLayer.lastReadyTileCount += 1 continue } const parentFallback = this.tileStore.getParentFallbackSlice(tile, scene) if (parentFallback) { this.drawEntry( parentFallback.entry, tile.url + '|parent', parentFallback.sourceX, parentFallback.sourceY, parentFallback.sourceWidth, parentFallback.sourceHeight, tile.leftPx, tile.topPx, tile.sizePx, tile.sizePx, scene, ) } const childFallback = this.tileStore.getChildFallback(tile, scene) if (!childFallback) { continue } const cellWidth = tile.sizePx / childFallback.division const cellHeight = tile.sizePx / childFallback.division for (const child of childFallback.children) { this.drawEntry( child.entry, tile.url + '|child|' + child.offsetX + '|' + child.offsetY, 0, 0, child.entry.image.width || 256, child.entry.image.height || 256, tile.leftPx + child.offsetX * cellWidth, tile.topPx + child.offsetY * cellHeight, cellWidth, cellHeight, scene, ) } } } drawEntry( entry: TileStoreEntry, cacheKey: string, sourceX: number, sourceY: number, sourceWidth: number, sourceHeight: number, drawLeft: number, drawTop: number, drawWidth: number, drawHeight: number, scene: MapScene, ): void { if (!this.gl || !entry.image) { return } const texture = this.getTexture(cacheKey, entry) if (!texture) { return } const gl = this.gl const imageWidth = entry.image.width || 256 const imageHeight = entry.image.height || 256 const texLeft = sourceX / imageWidth const texTop = sourceY / imageHeight const texRight = (sourceX + sourceWidth) / imageWidth const texBottom = (sourceY + sourceHeight) / imageHeight const topLeft = this.transformToClip(drawLeft, drawTop, scene) const topRight = this.transformToClip(drawLeft + drawWidth, drawTop, scene) const bottomLeft = this.transformToClip(drawLeft, drawTop + drawHeight, scene) const bottomRight = this.transformToClip(drawLeft + drawWidth, drawTop + drawHeight, scene) const positions = new Float32Array([ topLeft.x, topLeft.y, topRight.x, topRight.y, bottomLeft.x, bottomLeft.y, bottomLeft.x, bottomLeft.y, topRight.x, topRight.y, bottomRight.x, bottomRight.y, ]) const texCoords = new Float32Array([ texLeft, texTop, texRight, texTop, texLeft, texBottom, texLeft, texBottom, texRight, texTop, texRight, texBottom, ]) gl.bindTexture(gl.TEXTURE_2D, texture.texture) gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer) gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STREAM_DRAW) gl.enableVertexAttribArray(this.positionLocation) gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0) gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer) gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STREAM_DRAW) gl.enableVertexAttribArray(this.texCoordLocation) gl.vertexAttribPointer(this.texCoordLocation, 2, gl.FLOAT, false, 0, 0) gl.drawArrays(gl.TRIANGLES, 0, 6) } getTexture(cacheKey: string, entry: TileStoreEntry): TextureRecord | null { if (!this.gl || !entry.image) { return null } const key = cacheKey + '|' + entry.sourcePath const existing = this.textureCache.get(key) if (existing) { return existing } const texture = this.gl.createTexture() if (!texture) { return null } this.gl.bindTexture(this.gl.TEXTURE_2D, texture) this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE) this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE) this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR) this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR) this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1) this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, entry.image) const record = { key, texture } this.textureCache.set(key, record) return record } transformToClip(x: number, y: number, scene: MapScene): ScreenPoint { const rotated = rotateScreenPoint( { x, y }, scene.viewportWidth / 2, scene.viewportHeight / 2, scene.rotationRad || 0, ) const translated = { x: rotated.x + scene.translateX, y: rotated.y + scene.translateY, } const previewed = this.applyPreview(translated.x, translated.y, scene) return { x: this.toClipX(previewed.x, scene.viewportWidth), y: this.toClipY(previewed.y, scene.viewportHeight), } } applyPreview(x: number, y: number, scene: MapScene): { x: number; y: number } { const scale = scene.previewScale || 1 const originX = scene.previewOriginX || scene.viewportWidth / 2 const originY = scene.previewOriginY || scene.viewportHeight / 2 return { x: originX + (x - originX) * scale, y: originY + (y - originY) * scale, } } toClipX(x: number, width: number): number { return x / width * 2 - 1 } toClipY(y: number, height: number): number { return 1 - y / height * 2 } }