webglTileRenderer.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import { rotateScreenPoint, type ScreenPoint } from '../camera/camera'
  2. import { type TileStore, type TileStoreEntry } from '../tile/tileStore'
  3. import { TileLayer } from '../layer/tileLayer'
  4. import { buildCamera, type MapScene } from './mapRenderer'
  5. interface TextureRecord {
  6. key: string
  7. texture: any
  8. }
  9. function createShader(gl: any, type: number, source: string): any {
  10. const shader = gl.createShader(type)
  11. if (!shader) {
  12. throw new Error('WebGL shader 创建失败')
  13. }
  14. gl.shaderSource(shader, source)
  15. gl.compileShader(shader)
  16. if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
  17. const message = gl.getShaderInfoLog(shader) || 'unknown shader error'
  18. gl.deleteShader(shader)
  19. throw new Error(message)
  20. }
  21. return shader
  22. }
  23. function createProgram(gl: any, vertexSource: string, fragmentSource: string): any {
  24. const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
  25. const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
  26. const program = gl.createProgram()
  27. if (!program) {
  28. throw new Error('WebGL program 创建失败')
  29. }
  30. gl.attachShader(program, vertexShader)
  31. gl.attachShader(program, fragmentShader)
  32. gl.linkProgram(program)
  33. gl.deleteShader(vertexShader)
  34. gl.deleteShader(fragmentShader)
  35. if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  36. const message = gl.getProgramInfoLog(program) || 'unknown program error'
  37. gl.deleteProgram(program)
  38. throw new Error(message)
  39. }
  40. return program
  41. }
  42. export class WebGLTileRenderer {
  43. canvas: any
  44. gl: any
  45. tileLayer: TileLayer
  46. tileStore: TileStore
  47. dpr: number
  48. program: any
  49. positionBuffer: any
  50. texCoordBuffer: any
  51. positionLocation: number
  52. texCoordLocation: number
  53. textureCache: Map<string, TextureRecord>
  54. constructor(tileLayer: TileLayer, tileStore: TileStore) {
  55. this.canvas = null
  56. this.gl = null
  57. this.tileLayer = tileLayer
  58. this.tileStore = tileStore
  59. this.dpr = 1
  60. this.program = null
  61. this.positionBuffer = null
  62. this.texCoordBuffer = null
  63. this.positionLocation = -1
  64. this.texCoordLocation = -1
  65. this.textureCache = new Map<string, TextureRecord>()
  66. }
  67. attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
  68. this.canvas = canvasNode
  69. this.dpr = dpr || 1
  70. canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
  71. canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
  72. const gl = canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl')
  73. if (!gl) {
  74. throw new Error('当前环境不支持 WebGL')
  75. }
  76. this.gl = gl
  77. this.program = createProgram(
  78. gl,
  79. '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; }',
  80. 'precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); }',
  81. )
  82. this.positionBuffer = gl.createBuffer()
  83. this.texCoordBuffer = gl.createBuffer()
  84. this.positionLocation = gl.getAttribLocation(this.program, 'a_position')
  85. this.texCoordLocation = gl.getAttribLocation(this.program, 'a_texCoord')
  86. gl.viewport(0, 0, canvasNode.width, canvasNode.height)
  87. gl.disable(gl.DEPTH_TEST)
  88. gl.enable(gl.BLEND)
  89. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
  90. this.tileStore.attachCanvas(canvasNode)
  91. }
  92. destroy(): void {
  93. if (this.gl) {
  94. this.textureCache.forEach((record) => {
  95. this.gl && this.gl.deleteTexture(record.texture)
  96. })
  97. if (this.program) {
  98. this.gl.deleteProgram(this.program)
  99. }
  100. if (this.positionBuffer) {
  101. this.gl.deleteBuffer(this.positionBuffer)
  102. }
  103. if (this.texCoordBuffer) {
  104. this.gl.deleteBuffer(this.texCoordBuffer)
  105. }
  106. }
  107. this.textureCache.clear()
  108. this.program = null
  109. this.positionBuffer = null
  110. this.texCoordBuffer = null
  111. this.gl = null
  112. this.canvas = null
  113. }
  114. render(scene: MapScene): void {
  115. if (!this.gl || !this.program || !this.positionBuffer || !this.texCoordBuffer) {
  116. return
  117. }
  118. const gl = this.gl
  119. const camera = buildCamera(scene)
  120. const tiles = this.tileLayer.prepareTiles(scene, camera, this.tileStore)
  121. gl.viewport(0, 0, this.canvas.width, this.canvas.height)
  122. gl.clearColor(0.8588, 0.9333, 0.8314, 1)
  123. gl.clear(gl.COLOR_BUFFER_BIT)
  124. gl.useProgram(this.program)
  125. for (const tile of tiles) {
  126. const readyEntry = this.tileStore.getEntry(tile.url)
  127. if (readyEntry && readyEntry.status === 'ready' && readyEntry.image) {
  128. this.drawEntry(readyEntry, tile.url, 0, 0, readyEntry.image.width || 256, readyEntry.image.height || 256, tile.leftPx, tile.topPx, tile.sizePx, tile.sizePx, scene)
  129. this.tileLayer.lastReadyTileCount += 1
  130. continue
  131. }
  132. const parentFallback = this.tileStore.getParentFallbackSlice(tile, scene)
  133. if (parentFallback) {
  134. this.drawEntry(
  135. parentFallback.entry,
  136. tile.url + '|parent',
  137. parentFallback.sourceX,
  138. parentFallback.sourceY,
  139. parentFallback.sourceWidth,
  140. parentFallback.sourceHeight,
  141. tile.leftPx,
  142. tile.topPx,
  143. tile.sizePx,
  144. tile.sizePx,
  145. scene,
  146. )
  147. }
  148. const childFallback = this.tileStore.getChildFallback(tile, scene)
  149. if (!childFallback) {
  150. continue
  151. }
  152. const cellWidth = tile.sizePx / childFallback.division
  153. const cellHeight = tile.sizePx / childFallback.division
  154. for (const child of childFallback.children) {
  155. this.drawEntry(
  156. child.entry,
  157. tile.url + '|child|' + child.offsetX + '|' + child.offsetY,
  158. 0,
  159. 0,
  160. child.entry.image.width || 256,
  161. child.entry.image.height || 256,
  162. tile.leftPx + child.offsetX * cellWidth,
  163. tile.topPx + child.offsetY * cellHeight,
  164. cellWidth,
  165. cellHeight,
  166. scene,
  167. )
  168. }
  169. }
  170. }
  171. drawEntry(
  172. entry: TileStoreEntry,
  173. cacheKey: string,
  174. sourceX: number,
  175. sourceY: number,
  176. sourceWidth: number,
  177. sourceHeight: number,
  178. drawLeft: number,
  179. drawTop: number,
  180. drawWidth: number,
  181. drawHeight: number,
  182. scene: MapScene,
  183. ): void {
  184. if (!this.gl || !entry.image) {
  185. return
  186. }
  187. const texture = this.getTexture(cacheKey, entry)
  188. if (!texture) {
  189. return
  190. }
  191. const gl = this.gl
  192. const imageWidth = entry.image.width || 256
  193. const imageHeight = entry.image.height || 256
  194. const texLeft = sourceX / imageWidth
  195. const texTop = sourceY / imageHeight
  196. const texRight = (sourceX + sourceWidth) / imageWidth
  197. const texBottom = (sourceY + sourceHeight) / imageHeight
  198. const topLeft = this.transformToClip(drawLeft, drawTop, scene)
  199. const topRight = this.transformToClip(drawLeft + drawWidth, drawTop, scene)
  200. const bottomLeft = this.transformToClip(drawLeft, drawTop + drawHeight, scene)
  201. const bottomRight = this.transformToClip(drawLeft + drawWidth, drawTop + drawHeight, scene)
  202. const positions = new Float32Array([
  203. topLeft.x, topLeft.y,
  204. topRight.x, topRight.y,
  205. bottomLeft.x, bottomLeft.y,
  206. bottomLeft.x, bottomLeft.y,
  207. topRight.x, topRight.y,
  208. bottomRight.x, bottomRight.y,
  209. ])
  210. const texCoords = new Float32Array([
  211. texLeft, texTop,
  212. texRight, texTop,
  213. texLeft, texBottom,
  214. texLeft, texBottom,
  215. texRight, texTop,
  216. texRight, texBottom,
  217. ])
  218. gl.bindTexture(gl.TEXTURE_2D, texture.texture)
  219. gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer)
  220. gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STREAM_DRAW)
  221. gl.enableVertexAttribArray(this.positionLocation)
  222. gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0)
  223. gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer)
  224. gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STREAM_DRAW)
  225. gl.enableVertexAttribArray(this.texCoordLocation)
  226. gl.vertexAttribPointer(this.texCoordLocation, 2, gl.FLOAT, false, 0, 0)
  227. gl.drawArrays(gl.TRIANGLES, 0, 6)
  228. }
  229. getTexture(cacheKey: string, entry: TileStoreEntry): TextureRecord | null {
  230. if (!this.gl || !entry.image) {
  231. return null
  232. }
  233. const key = cacheKey + '|' + entry.sourcePath
  234. const existing = this.textureCache.get(key)
  235. if (existing) {
  236. return existing
  237. }
  238. const texture = this.gl.createTexture()
  239. if (!texture) {
  240. return null
  241. }
  242. this.gl.bindTexture(this.gl.TEXTURE_2D, texture)
  243. this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
  244. this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
  245. this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
  246. this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR)
  247. this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1)
  248. this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, entry.image)
  249. const record = { key, texture }
  250. this.textureCache.set(key, record)
  251. return record
  252. }
  253. transformToClip(x: number, y: number, scene: MapScene): ScreenPoint {
  254. const rotated = rotateScreenPoint(
  255. { x, y },
  256. scene.viewportWidth / 2,
  257. scene.viewportHeight / 2,
  258. scene.rotationRad || 0,
  259. )
  260. const translated = {
  261. x: rotated.x + scene.translateX,
  262. y: rotated.y + scene.translateY,
  263. }
  264. const previewed = this.applyPreview(translated.x, translated.y, scene)
  265. return {
  266. x: this.toClipX(previewed.x, scene.viewportWidth),
  267. y: this.toClipY(previewed.y, scene.viewportHeight),
  268. }
  269. }
  270. applyPreview(x: number, y: number, scene: MapScene): { x: number; y: number } {
  271. const scale = scene.previewScale || 1
  272. const originX = scene.previewOriginX || scene.viewportWidth / 2
  273. const originY = scene.previewOriginY || scene.viewportHeight / 2
  274. return {
  275. x: originX + (x - originX) * scale,
  276. y: originY + (y - originY) * scale,
  277. }
  278. }
  279. toClipX(x: number, width: number): number {
  280. return x / width * 2 - 1
  281. }
  282. toClipY(y: number, height: number): number {
  283. return 1 - y / height * 2
  284. }
  285. }