| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 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<string, TextureRecord>
- 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<string, TextureRecord>()
- }
- 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
- }
- }
|