Files
cmr-mini/miniprogram/engine/renderer/webglTileRenderer.ts

330 lines
10 KiB
TypeScript

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