import { mat4 } from 'gl-matrix'; import { logger } from '../common/logger'; import { WebGLColorArray, createProgram, isWebGL2Supported } from './webgl'; import { setCanvasFilterByFloorId } from '@/plugin/fx/gameCanvas'; import { ILayerRenderExtends, Layer } from '../render/preset/layer'; import { HeroRenderer } from '../render/preset/hero'; import { Sprite } from '../render/sprite'; /** * 最大光源数量,必须设置,且光源数不能超过这个值,这个值决定了会预留多少的缓冲区,因此最好尽可能小,同时游戏过程中不可修改 * 这个值越大,对显卡尤其是显存的要求会越大,不过考虑到各种设备的性能差异,不建议超过10 */ const MAX_LIGHT_NUM = 5; /** 阴影层的Z值 */ const Z_INDEX = 55; // 我也不知道这个数怎么来的,试出来是这个,别动就行 const FOVY = Math.PI / 2; const ignore: Set = new Set([660, 661]); interface LightConfig { decay: number; r: number; color: WebGLColorArray; noShelter?: boolean; } interface ShadowConfig { background?: WebGLColorArray; immerse?: number; blur?: number; } function addLightFromBlock(floors: FloorIds[], block: number, config: LightConfig, sc?: ShadowConfig, hero?: LightConfig) { floors.forEach(v => { const shadow = new Shadow(v); shadow.background = [0, 0, 0, 0.2]; if (sc) { if (sc.background) shadow.background = sc.background; if (sc.blur) shadow.blur = sc.blur; if (sc.immerse) shadow.immerse = sc.immerse; } if (hero) { shadow.addLight({ id: `${v}_hero`, x: 0, y: 0, decay: hero.decay, r: hero.r, color: hero.color, followHero: true, noShelter: hero.noShelter }); } core.floors[v].map.forEach((arr, y) => { arr.forEach((num, x) => { if (num === block) { shadow.addLight({ id: `${v}_${x}_${y}`, x: x * 32 + 16, y: y * 32 + 16, decay: config.decay, r: config.r, color: config.color, noShelter: config.noShelter }); } }); }); }) } const hook = Mota.require('var', 'hook'); hook.once('reset', () => { Shadow.init(); addLightFromBlock( core.floorIds.slice(61, 70).concat(core.floorIds.slice(72, 81)).concat(core.floorIds.slice(85, 103)), 103, { decay: 50, r: 300, color: [0.9333, 0.6, 0.333, 0.3] }, { background: [0, 0, 0, 0.26] }, { decay: 50, r: 250, color: [0, 0, 0, 0] } ); addLightFromBlock( ['MT50', 'MT60', 'MT61', 'MT72', 'MT73', 'MT74', 'MT75'], 103, { decay: 20, r: 150, color: [0.9333, 0.6, 0.333, 0.4], noShelter: true }, { background: [0, 0, 0, 0.4] } ); // Shadow.mount(); // 勇士身上的光源 // Mota.rewrite(core.control, 'drawHero', 'add', () => { // if (core.getFlag('__heroOpacity__') !== 0) { // const shadow = Shadow.now(); // if (shadow) { // shadow.followHero.forEach(v => { // shadow.modifyLight(v, { // x: core.status.heroCenter.px, // y: core.status.heroCenter.py + 8 // }); // }); // if (shadow.followHero.size > 0) shadow.requestRefresh(); // } // } // }); // 更新地形数据 // Mota.rewrite(core.maps, 'removeBlock', 'add', success => { // if (success && !main.replayChecking) { // Shadow.update(true); // } // return success; // }); // Mota.rewrite(core.maps, 'setBlock', 'add', () => { // if (!main.replayChecking) { // Shadow.update(true); // } // }); Mota.rewrite(core.control, 'loadData', 'add', () => { if (!main.replayChecking) { Shadow.update(true); } }); // Mota.require('var', 'hook').on('changingFloor', (floorId) => { // if (!main.replayChecking) { // Shadow.clearBuffer(); // Shadow.update(); // setCanvasFilterByFloorId(floorId); // } // }) }); hook.on('reset', () => { Shadow.update(true); LayerShadowExtends.shadowList.forEach(v => v.sprite.update(v.sprite)); }) hook.on('setBlock', () => { Shadow.update(true); LayerShadowExtends.shadowList.forEach(v => v.sprite.update(v.sprite)); }) hook.on('changingFloor', floorId => { Shadow.clearBuffer(); Shadow.update(); setCanvasFilterByFloorId(floorId); LayerShadowExtends.shadowList.forEach(v => v.sprite.update(v.sprite)); }) // 深度测试着色器 const depthVertex = /* glsl */ ` precision mediump float; attribute vec4 a_position; uniform mat4 u_projection; uniform mat4 u_view; void main() { gl_Position = u_projection * u_view * a_position; } `; const depthFragment = /* glsl */ ` void main() { // 深度测试中不需要片元着色器 gl_FragColor = vec4(0.7, 0.7, 0.7, 1.0); } `; // 渲染着色器 const colorVertex = /* glsl */ `#version 300 es precision mediump float; in vec2 a_position; in vec2 a_texcoord; out vec2 v_texcoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texcoord = a_texcoord; } `; const colorFragment = /* glsl */ `#version 300 es precision mediump float; precision mediump sampler2DArray; in vec2 v_texcoord; uniform sampler2DArray u_depthTexture; // 深度检测结果 uniform vec4 u_background; // 背景色 uniform int u_lightCount; uniform vec2 u_screen; // 画布大小信息 layout (std140) uniform LightInfo { vec2 pos[${MAX_LIGHT_NUM}]; // 光源坐标 vec4 color[${MAX_LIGHT_NUM}]; // 光源颜色 vec3 decay[${MAX_LIGHT_NUM}]; // 光源半径、开始衰减半径、是否会被遮挡 }; out vec4 outColor; vec4 blend(vec4 color1, vec4 color2) { vec3 co = color2.rgb * color2.a + color1.rgb * color1.a * (1.0 - color2.a); float ao = color2.a + color1.a * (1.0 - color2.a); return vec4(co, ao); } void main() { vec4 lightColor = vec4(0.0, 0.0, 0.0, 0.0); float strengthTotal = 0.0; for (int i = 0; i < u_lightCount; i++) { vec2 p = pos[i]; vec4 c = color[i]; vec3 d = decay[i]; vec2 loc = vec2((gl_FragCoord.x - p.x) / u_screen.x / 2.0 + 0.5, (gl_FragCoord.y - p.y) / u_screen.y / 2.0 + 0.5); float sheltered = texture(u_depthTexture, vec3(loc, i)).a; float dis = distance(gl_FragCoord.xy, p); // 计算距离 float strength = clamp((dis - d.r) / (d.g - d.r), 0.0, 1.0); // 限制强度范围 if (sheltered > 0.5 && d.z < 0.5) strength = 0.0; // 遮挡逻辑 strengthTotal += strength; // 累计强度 lightColor = mix(lightColor, vec4(c.rgb, c.a), strength); // 混合光源颜色 } if (strengthTotal > 1.0) strengthTotal = 1.0; outColor = blend(vec4(u_background.rgb, u_background.a * (1.0 - strengthTotal)), lightColor); } `; // 高斯模糊着色器,顶点着色器依然可以使用colorVertex const blur1Fragment = /* glsl */ `#version 300 es precision mediump float; uniform sampler2D u_texture; // 输入纹理 uniform float u_blurRadius; // 模糊半径 uniform vec2 u_textureSize; // 纹理的大小 in vec2 v_texcoord; // 接受顶点着色器传递的纹理坐标 out vec4 fragColor; // 输出颜色 // 计算高斯权重 float gaussian(float x, float sigma) { return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * 3.141592653589793) * sigma); } void main() { float sigma = u_blurRadius / 3.0; // 标准差 int kernelSize = int(u_blurRadius) * 2 + 1; // 高斯核的大小 float sum = 0.0; // 权重总和 vec4 color = vec4(0.0); // 初始颜色 for (int i = -int(u_blurRadius); i <= int(u_blurRadius); i++) { float weight = gaussian(float(i), sigma); // 计算权重 sum += weight; // 权重累积 vec2 offset = vec2(i, 0) / u_textureSize; // 水平方向偏移 float x = v_texcoord.x + offset.x; float y = v_texcoord.y + offset.y; if (x < 0.0 || y < 0.0 || x > 1.0 || y > 1.0) continue; color += texture(u_texture, vec2(x, y)) * weight; // 采样并加权 } fragColor = color / sum; // 归一化结果 } `; const blur2Fragment = /* glsl */ `#version 300 es precision mediump float; uniform sampler2D u_texture; // 输入纹理 uniform float u_blurRadius; // 模糊半径 uniform vec2 u_textureSize; // 纹理的大小 in vec2 v_texcoord; // 接受顶点着色器传递的纹理坐标 out vec4 fragColor; // 输出颜色 // 计算高斯权重 float gaussian(float x, float sigma) { return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * 3.141592653589793) * sigma); } void main() { float sigma = u_blurRadius / 3.0; // 标准差 int kernelSize = int(u_blurRadius) * 2 + 1; // 高斯核的大小 float sum = 0.0; // 权重总和 vec4 color = vec4(0.0); // 初始颜色 for (int i = -int(u_blurRadius); i <= int(u_blurRadius); i++) { float weight = gaussian(float(i), sigma); // 计算权重 sum += weight; // 权重累积 vec2 offset = vec2(0, i) / u_textureSize; // 垂直方向偏移 color += texture(u_texture, v_texcoord + offset) * weight; // 采样并加权 } fragColor = color / sum; // 归一化结果 } `; interface ShadowProgram { depth: WebGLProgram; color: WebGLProgram; blur1: WebGLProgram; blur2: WebGLProgram; } interface ShadowCache { position: Float32Array; } interface LightInfo { id: string; x: number; y: number; r: number; decay: number; color: WebGLColorArray; noShelter?: boolean; followHero?: boolean; } interface ShadowLocations { depth: { a_position: number; u_projection: WebGLUniformLocation; u_view: WebGLUniformLocation; }; color: { a_position: number; a_texcoord: number; u_background: WebGLUniformLocation; u_lightCount: WebGLUniformLocation; u_screen: WebGLUniformLocation; u_depthTexture: WebGLUniformLocation; LightInfo: number; }; blur1: { a_position: number; a_texcoord: number; u_texture: WebGLUniformLocation; u_textureSize: WebGLUniformLocation; u_blurRadius: WebGLUniformLocation; }; blur2: { a_position: number; a_texcoord: number; u_texture: WebGLUniformLocation; u_textureSize: WebGLUniformLocation; u_blurRadius: WebGLUniformLocation; }; } interface ShadowMatrix { projection: mat4; } interface ShadowBuffer { depth: { position: WebGLBuffer; framebuffer: WebGLFramebuffer[]; }; color: { position: WebGLBuffer; texcoord: WebGLBuffer; indices: WebGLBuffer; lights: WebGLBuffer; framebuffer: WebGLFramebuffer; }; blur1: { position: WebGLBuffer; texcoord: WebGLBuffer; indices: WebGLBuffer; framebuffer: WebGLFramebuffer; }; blur2: { position: WebGLBuffer; texcoord: WebGLBuffer; indices: WebGLBuffer; }; } interface ShadowTexture { depth: WebGLTexture; color: WebGLTexture; blur: WebGLTexture; } export class Shadow { static canvas: HTMLCanvasElement; static gl: WebGL2RenderingContext; static program: ShadowProgram; static map: Partial> = {}; private static locations: ShadowLocations; private static martix: ShadowMatrix; private static buffer: ShadowBuffer; private static texture: ShadowTexture; private static cached: Set = new Set(); floorId: FloorIds; lights: LightInfo[] = []; immerse: number = 4; blur: number = 4; background: WebGLColorArray = [0, 0, 0, 0]; originLightInfo: Record = {}; followHero: Set = new Set(); private cache?: ShadowCache; private needRefresh: boolean = false; private refreshNoCache: boolean = false; constructor(floor: FloorIds) { this.floorId = floor; Shadow.map[floor] = this; } /** * 计算墙壁的立体信息,用于深度测试 * @param nocache 是否不使用缓存 */ calShadowInfo(nocache: boolean = false) { if (!nocache && this.cache && Shadow.cached.has(this.floorId)) return this.cache; Shadow.cached.add(this.floorId); Shadow.clearBuffer(); const polygons = calMapPolygons(this.floorId, this.immerse, nocache); const res: number[] = []; const ratio = devicePixelRatio * core.domStyle.scale; const m = core._PX_ * ratio * 2; polygons.forEach(v => { v.forEach(([x, y, w, h]) => { const l = x * ratio; const b = (core._PY_ - y) * ratio; const r = (x + w) * ratio; const t = (core._PY_ - (y + h)) * ratio; res.push( // 上边缘 l, t, 0, l, t, m, r, t, m, r, t, 0, r, t, m, l, t, 0, // 右 r, t, 0, r, t, m, r, b, m, r, b, 0, r, b, m, r, t, 0, // 下 r, b, 0, r, b, m, l, b, m, l, b, 0, l, b, m, r, b, 0, // 左 l, b, 0, l, b, m, l, t, m, l, t, 0, l, t, m, l, b, 0 ); }); }); return (this.cache = { position: new Float32Array(res) }); } /** * 添加一个光源 */ addLight(info: LightInfo) { if (this.originLightInfo[info.id]) { logger.warn(7, `Repeated light id.`); return; } this.originLightInfo[info.id] = info; this.lights.push({ ...info, y: core._PY_ - info.y }); if (info.followHero) { this.followHero.add(info.id); } this.requestRefresh(); } /** * 移除一个光源 * @param id 要移除的光源id */ removeLight(id: string) { const index = this.lights.findIndex(v => v.id === id); this.lights.splice(index, 1); delete this.originLightInfo[id] this.followHero.delete(id); this.requestRefresh(); } /** * 修改光源信息,注意不能直接对光源操作,如果需要操作务必使用本函数 * @param id 要修改的光源id * @param light 修改的信息,例如想修改x就填 { x: 100 } */ modifyLight(id: string, light: Partial) { const origin = this.originLightInfo[id]; const l = this.lights.find(v => v.id === id); if (!origin || !l) return; for (const [key, value] of Object.entries(light)) { const k = key as keyof LightInfo; // @ts-ignore origin[k] = value; if (k === 'y') { l.y = core._PY_ - (value as number); } else { // @ts-ignore l[k] = value; } } } /** * 刷新阴影信息,并重新渲染 * @param nocahce 是否不使用缓存 */ requestRefresh(nocahce: boolean = false) { if (core.status.floorId !== this.floorId) return; if (nocahce) this.refreshNoCache = true; if (this.needRefresh) return; requestAnimationFrame(() => { this.refresh(this.refreshNoCache); this.refreshNoCache = false; }); } /** * 渲染阴影 * @param nocache 是否不使用缓存 * @param resize 是否resize canvas */ private refresh(nocache: boolean = false, resize: boolean = false) { this.calShadowInfo(nocache); if (resize) { Shadow.resizeCanvas(); } // clear const canvas = Shadow.canvas; const gl = Shadow.gl; const ratio = core.domStyle.scale * devicePixelRatio; gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.clearColor(0.0, 0.0, 0.0, 0.0); gl.viewport(0, 0, canvas.width, canvas.height); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ZERO); // depth test gl.useProgram(Shadow.program.depth); const lightProjection = Shadow.martix.projection // 使用 3D 纹理存储深度信息 const texture = Shadow.texture.depth; const info = this.calShadowInfo(nocache).position; const length = info.length; const positionBuffer = Shadow.buffer.depth.position; const position = Shadow.locations.depth.a_position; gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, info, gl.STATIC_DRAW); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 3, gl.FLOAT, false, 0, 0); const proj = Shadow.locations.depth.u_projection; const view = Shadow.locations.depth.u_view; gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); this.lights.forEach((light, i) => { this.depthTest(lightProjection, light, i, texture, length, proj, view); }); gl.disableVertexAttribArray(position); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(info.length), gl.STATIC_DRAW); gl.vertexAttribPointer(position, 3, gl.FLOAT, false, 0, 0); gl.bindTexture(gl.TEXTURE_2D, null); // render gl.useProgram(Shadow.program.color); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.bindFramebuffer(gl.FRAMEBUFFER, null); // Buffers const posColor = Shadow.locations.color.a_position; const texColor = Shadow.locations.color.a_texcoord; const posColorBuffer = Shadow.buffer.color.position; gl.bindBuffer(gl.ARRAY_BUFFER, posColorBuffer); gl.enableVertexAttribArray(posColor); gl.vertexAttribPointer(posColor, 2, gl.FLOAT, false, 0, 0); const texColorBuffer = Shadow.buffer.color.texcoord; gl.bindBuffer(gl.ARRAY_BUFFER, texColorBuffer); gl.enableVertexAttribArray(texColor); gl.vertexAttribPointer(texColor, 2, gl.FLOAT, false, 0, 0); const buffer = Shadow.buffer.color.indices; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); // Background & lightCount const backgroundPos = Shadow.locations.color.u_background; gl.uniform4f(backgroundPos, ...this.background); const lightCountPos = Shadow.locations.color.u_lightCount; gl.uniform1i(lightCountPos, this.lights.length); const screenPos = Shadow.locations.color.u_screen; gl.uniform2f(screenPos, canvas.width, canvas.height); // Texture const textureLoc = Shadow.locations.color.u_depthTexture; gl.uniform1i(textureLoc, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture); // UBO const data = []; for (let i = 0; i < MAX_LIGHT_NUM; i++) { if (this.lights[i]) { const v = this.lights[i]; data.push( // 坐标 v.x * ratio, v.y * ratio, 0, 0 // 填充到 4 个分量以确保对齐 ); } else { // 如果没有光源,添加填充以确保统一缓冲区大小保持一致 data.push( 0, 0, 0, 0 ); } } for (let i = 0; i < MAX_LIGHT_NUM; i++) { if (this.lights[i]) { const v = this.lights[i]; data.push( // 颜色 v.color[0], v.color[1], v.color[2], v.color[3] // 4 个分量的颜色 ); } else { // 如果没有光源,添加填充以确保统一缓冲区大小保持一致 data.push( 0, 0, 0, 0 ); } } for (let i = 0; i < MAX_LIGHT_NUM; i++) { if (this.lights[i]) { const v = this.lights[i]; data.push( // 半径、衰减半径、遮挡 v.r * ratio, v.decay * ratio, v.noShelter ? 1 : 0, 0 // 填充到 4 个分量 ); } else { // 如果没有光源,添加填充以确保统一缓冲区大小保持一致 data.push( 0, 0, 0, 0 ); } } const blockIndex = Shadow.locations.color.LightInfo; const lightsBuffer = Shadow.buffer.color.lights; gl.bindBuffer(gl.UNIFORM_BUFFER, lightsBuffer); gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(data), gl.STATIC_DRAW); gl.uniformBlockBinding(Shadow.program.color, blockIndex, 0); gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, lightsBuffer); // Render to framebuffer const colorTexture = Shadow.texture.color; const colorFramebuffer = Shadow.buffer.color.framebuffer; gl.enable(gl.DEPTH_TEST); gl.bindFramebuffer(gl.FRAMEBUFFER, colorFramebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0); gl.bindTexture(gl.TEXTURE_2D, colorTexture); gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); gl.disableVertexAttribArray(posColor); gl.disableVertexAttribArray(texColor); // Apply blur gl.useProgram(Shadow.program.blur1); // Buffer const posBlur1 = Shadow.locations.blur1.a_position; const texBlur1 = Shadow.locations.blur1.a_texcoord; const posBlur1Buffer = Shadow.buffer.blur1.position; gl.bindBuffer(gl.ARRAY_BUFFER, posBlur1Buffer); gl.enableVertexAttribArray(posBlur1); gl.vertexAttribPointer(posBlur1, 2, gl.FLOAT, false, 0, 0); const texBlur1Buffer = Shadow.buffer.blur1.texcoord; gl.bindBuffer(gl.ARRAY_BUFFER, texBlur1Buffer); gl.enableVertexAttribArray(texBlur1); gl.vertexAttribPointer(texBlur1, 2, gl.FLOAT, false, 0, 0); const blur1IndicesBuffer = Shadow.buffer.blur1.indices; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, blur1IndicesBuffer); const blur1Indices = new Uint16Array([0, 1, 2, 2, 3, 1]); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, blur1Indices, gl.STATIC_DRAW); // Texture const blur1TextureLoc = Shadow.locations.blur1.u_texture; const blur1TextureSizeLoc = Shadow.locations.blur1.u_textureSize; const blur1BlurRadiusLoc = Shadow.locations.blur1.u_blurRadius; gl.uniform1i(blur1TextureLoc, 0); gl.uniform1f(blur1BlurRadiusLoc, this.blur * ratio); gl.uniform2f(blur1TextureSizeLoc, canvas.width, canvas.height); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, colorTexture); gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); // Render blur const blurTexture = Shadow.texture.blur; const blurFramebuffer = Shadow.buffer.blur1.framebuffer; gl.bindTexture(gl.TEXTURE_2D, colorTexture); gl.bindFramebuffer(gl.FRAMEBUFFER, blurFramebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, blurTexture, 0); gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.disableVertexAttribArray(posBlur1); gl.disableVertexAttribArray(texBlur1); // Applay blur 2 gl.useProgram(Shadow.program.blur2); // Buffer const posBlur2 = Shadow.locations.blur2.a_position; const texBlur2 = Shadow.locations.blur2.a_texcoord; const posBlur2Buffer = Shadow.buffer.blur2.position; gl.bindBuffer(gl.ARRAY_BUFFER, posBlur2Buffer); gl.enableVertexAttribArray(posBlur1); gl.vertexAttribPointer(posBlur2, 2, gl.FLOAT, false, 0, 0); const textBlur2Buffer = Shadow.buffer.blur2.texcoord; gl.bindBuffer(gl.ARRAY_BUFFER, textBlur2Buffer); gl.enableVertexAttribArray(texBlur2); gl.vertexAttribPointer(texBlur2, 2, gl.FLOAT, false, 0, 0); const blur2IndicesBuffer = Shadow.buffer.blur2.indices; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, blur2IndicesBuffer); // Texture const blur2TextureLoc = Shadow.locations.blur2.u_texture; const blur2TextureSizeLoc = Shadow.locations.blur2.u_textureSize; const blur2BlurRadiusLoc = Shadow.locations.blur2.u_blurRadius; gl.uniform1i(blur2TextureLoc, 0); gl.uniform1f(blur2BlurRadiusLoc, this.blur * ratio); gl.uniform2f(blur2TextureSizeLoc, canvas.width, canvas.height); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, blurTexture); // Render to target gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); gl.bindTexture(gl.TEXTURE_2D, null); gl.disableVertexAttribArray(posBlur2); gl.disableVertexAttribArray(texBlur2); } private depthTest( lightProjection: mat4, light: LightInfo, index: number, texture: WebGLTexture, length: number, proj: WebGLUniformLocation, view: WebGLUniformLocation ) { const gl = Shadow.gl; const ratio = core.domStyle.scale * devicePixelRatio; const cameraMatrix = mat4.create(); mat4.lookAt(cameraMatrix, [light.x * ratio, light.y * ratio, core._PX_ * 2 * ratio], [light.x * ratio, light.y * ratio, 0], [0, 1, 0]); const size = core._PX_ * ratio * 2; gl.viewport(0, 0, size, size); const framebuffer = Shadow.buffer.depth.framebuffer[index]; gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, texture, 0, index); gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); gl.uniformMatrix4fv(proj, false, lightProjection); gl.uniformMatrix4fv(view, false, cameraMatrix); gl.drawArrays(gl.TRIANGLES, 0, length); gl.bindFramebuffer(gl.FRAMEBUFFER, null); return framebuffer; } private static create3DTexture(size: number, depth: number) { const gl = Shadow.gl; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture); gl.texImage3D( gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, size * 2, size * 2, depth, // 层数 0, gl.RGBA, gl.UNSIGNED_BYTE, null ); gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST); return texture!; } private static create2DTexture(size: number) { const gl = Shadow.gl; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, null ); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return texture!; } private static initBuffer(pos: Float32Array) { const gl = this.gl; const posBuffer = gl.createBuffer()!; gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); gl.bufferData(gl.ARRAY_BUFFER, pos, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); return posBuffer; } private static initIndicesBuffer() { const gl = this.gl; const buffer = gl.createBuffer()!; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); const indices = new Uint16Array([0, 1, 2, 2, 3, 1]); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); return buffer; } static resizeCanvas() { const canvas = this.canvas; const scale = core.domStyle.scale; const ratio = scale * devicePixelRatio; canvas.width = core._PX_ * ratio; canvas.height = core._PY_ * ratio; canvas.style.left = `0px`; canvas.style.top = `0px`; canvas.style.width = `${scale * core._PX_}px`; canvas.style.height = `${scale * core._PY_}px`; // Texture const gl = this.gl; gl.deleteTexture(this.texture.blur); gl.deleteTexture(this.texture.color); gl.deleteTexture(this.texture.depth); this.texture.blur = this.create2DTexture(canvas.width); this.texture.color = this.create2DTexture(canvas.width); this.texture.depth = this.create3DTexture(canvas.width, MAX_LIGHT_NUM); Shadow.cached.clear(); this.now()?.requestRefresh(); } /** * 初始化阴影绘制 */ static init() { const gl = this.gl; if (!isWebGL2Supported()) return; gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); const ratio = core.domStyle.scale * devicePixelRatio; // program const depth = createProgram(gl, depthVertex, depthFragment); const color = createProgram(gl, colorVertex, colorFragment); const blur1 = createProgram(gl, colorVertex, blur1Fragment); const blur2 = createProgram(gl, colorVertex, blur2Fragment); this.program = { depth, color, blur1, blur2 }; // canvas const canvas = this.canvas; canvas.id = `shadow`; canvas.style.display = 'block'; canvas.style.position = 'absolute'; canvas.style.pointerEvents = 'none'; canvas.style.zIndex = Z_INDEX.toString(); // Locations this.locations = { depth: { a_position: gl.getAttribLocation(depth, 'a_position')!, u_projection: gl.getUniformLocation(depth, 'u_projection')!, u_view: gl.getUniformLocation(depth, 'u_view')! }, color: { a_position: gl.getAttribLocation(color, 'a_position')!, a_texcoord: gl.getAttribLocation(color, 'a_texcoord')!, u_background: gl.getUniformLocation(color, 'u_background')!, u_depthTexture: gl.getUniformLocation(color, 'u_depthTexture')!, u_lightCount: gl.getUniformLocation(color, 'u_lightCount')!, u_screen: gl.getUniformLocation(color, 'u_screen')!, LightInfo: gl.getUniformBlockIndex(color, 'LightInfo') }, blur1: { a_position: gl.getAttribLocation(blur1, 'a_position')!, a_texcoord: gl.getAttribLocation(blur1, 'a_texcoord')!, u_blurRadius: gl.getUniformLocation(blur1, 'u_blurRadius')!, u_texture: gl.getUniformLocation(blur1, 'u_texture')!, u_textureSize: gl.getUniformLocation(blur1, 'u_textureSize')! }, blur2: { a_position: gl.getAttribLocation(blur2, 'a_position')!, a_texcoord: gl.getAttribLocation(blur2, 'a_texcoord')!, u_blurRadius: gl.getUniformLocation(blur2, 'u_blurRadius')!, u_texture: gl.getUniformLocation(blur2, 'u_texture')!, u_textureSize: gl.getUniformLocation(blur2, 'u_textureSize')! } } // Matrix const lightProjection = mat4.create(); mat4.perspective(lightProjection, FOVY, 1, 1, core._PX_ * ratio); this.martix = { projection: lightProjection } // Buffer const depthFramebuffers = Array(MAX_LIGHT_NUM).fill(1).map(v => gl.createFramebuffer()!); this.buffer = { depth: { position: gl.createBuffer()!, framebuffer: depthFramebuffers }, color: { position: this.initBuffer(new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])), texcoord: this.initBuffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])), indices: this.initIndicesBuffer(), framebuffer: gl.createFramebuffer()!, lights: gl.createBuffer()! }, blur1: { position: this.initBuffer(new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])), texcoord: this.initBuffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])), indices: this.initIndicesBuffer(), framebuffer: gl.createFramebuffer()! }, blur2: { position: this.initBuffer(new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])), texcoord: this.initBuffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])), indices: this.initIndicesBuffer() } } // Texture this.texture = { depth: this.create3DTexture(core._PX_, MAX_LIGHT_NUM), color: this.create2DTexture(core._PX_), blur: this.create2DTexture(core._PX_) } this.resizeCanvas(); } static resize() { if (this.martix) { const ratio = core.domStyle.scale * devicePixelRatio; const lightProjection = mat4.create(); mat4.perspective(lightProjection, FOVY, 1, 1, core._PX_ * ratio); this.martix = { projection: lightProjection } } } /** * 清除一些关键缓冲区的内容,当且仅当图块变化或者切换地图时调用 */ static clearBuffer() { const gl = this.gl; const canvas = this.canvas; gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer.depth.position); gl.bufferData(gl.ARRAY_BUFFER, 0, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.UNIFORM_BUFFER, this.buffer.color.lights); gl.bufferData(gl.UNIFORM_BUFFER, 0, gl.STATIC_DRAW); gl.bindBuffer(gl.UNIFORM_BUFFER, null); } static mount() { core.dom.gameDraw.appendChild(this.canvas); } /** * 更新当前的阴影绘制 * @param nocache 是否不使用缓存 */ static update(nocache: boolean = false) { const floor = core.status.floorId; this.map[floor]?.requestRefresh(nocache); if (!this.map[floor]) { this.canvas.style.display = 'none'; } else { this.canvas.style.display = 'block'; } } static now() { return this.map[core.status.floorId]; } } Shadow.canvas = document.createElement('canvas'); Shadow.gl = Shadow.canvas.getContext('webgl2')!; const wallCache: Partial> = {}; const polygonCache: Partial< Record > = {}; const requiredCls = ['terrains', 'autotile', 'tileset', 'animates']; interface PolygonStack { x: number; y: number; dir: Dir; } /** * 计算一个地图的墙壁组成的多边形 */ export function calMapPolygons( floor: FloorIds, immerse: number = 4, nocache: boolean = false ) { if (!nocache && polygonCache[floor]) return polygonCache[floor]!; const wall = calMapWalls(floor, nocache); const used: Set = new Set(); const { width } = core.floors[floor]; const res: [number, number, number, number][][] = []; for (const nodes of wall) { used.clear(); if (nodes.length === 1) { const [x, y] = nodes[0]; res.push([ [ x * 32 + immerse, y * 32 + immerse, 32 - immerse * 2, 32 - immerse * 2 ] ]); } const walls = new Set(nodes.map(([x, y]) => x + y * width)); const arr: [number, number, number, number][] = []; const [fx, fy] = nodes[0]; // 不那么标准的dfs,一条线走到黑,然后再看分支 // 这么做的目的是尽量增大矩形的面积,减少节点数 const stack: PolygonStack[] = []; let f = false; for (const [dir, { x: dx, y: dy }] of Object.entries(core.utils.scan)) { const nx = fx + dx; const ny = fy + dy; if (walls.has(nx + ny * width)) { stack.unshift({ x: f ? nx : fx, y: f ? ny : fy, dir: dir as Dir }); f = true; } } while (stack.length > 0) { const { x, y, dir } = stack.pop()!; const { x: dx, y: dy } = core.utils.scan[dir]; let nx = x; let ny = y; let hasForward = false; if (used.has(x + y * width)) continue; used.add(x + y * width); while (1) { if (!walls.has(nx + ny * width)) { break; } let hasNext = false; for (const [d, { x: ddx, y: ddy }] of Object.entries( core.utils.scan )) { const nnx = ddx + nx; const nny = ddy + ny; if (nnx < 0 || nny < 0 || nnx >= width) continue; const num = nnx + nny * width; if (walls.has(num)) { if (dir === d) { hasNext = true; if (used.has(num)) hasForward = true; } else if (!used.has(num)) { stack.push({ x: nnx, y: nny, dir: d as Dir }); } } } if (!hasNext || hasForward) break; nx += dx; ny += dy; used.add(nx + ny * width); } const bx = x - dx; const by = y - dy; let hasBack = walls.has(bx + by * width) && used.has(bx + by * width); // 纯纯的数学计算,别动就行了 switch (dir) { case 'up': { let sy = ny * 32 + immerse; let sh = (y - ny + 1) * 32 - immerse * 2; if (hasForward) { sy -= immerse * 2; sh += immerse * 2; } if (hasBack) sh += immerse * 2; arr.push([nx * 32 + immerse, sy, 32 - immerse * 2, sh]); break; } case 'right': { let sx = x * 32 + immerse; let sw = (nx - x + 1) * 32 - immerse * 2; if (hasForward) sw += immerse * 2; if (hasBack) { sx -= immerse * 2; sw += immerse * 2; } arr.push([sx, y * 32 + immerse, sw, 32 - immerse * 2]); break; } case 'down': { let sy = y * 32 + immerse; let sh = (ny - y + 1) * 32 - immerse * 2; if (hasForward) sh += immerse * 2; if (hasBack) { sy -= immerse * 2; sh += immerse * 2; } arr.push([x * 32 + immerse, sy, 32 - immerse * 2, sh]); break; } case 'left': { let sx = nx * 32 + immerse; let sw = (x - nx + 1) * 32 - immerse * 2; if (hasForward) { sx -= immerse * 2; sw += immerse * 2; } if (hasBack) sw += immerse * 2; arr.push([sx, ny * 32 + immerse, sw, 32 - immerse * 2]); break; } } } res.push(arr); } polygonCache[floor] = res; return res; } /** * 计算一个地图的墙壁连接情况 */ export function calMapWalls(floor: FloorIds, nocache: boolean = false) { if (!nocache && wallCache[floor]) return wallCache[floor]!; const used: Set = new Set(); const obj = core.getMapBlocksObj(floor); const { width } = core.floors[floor]; const res: [number, number][][] = []; for (const block of Object.values(obj)) { const { x, y } = block; if ( !used.has(x + y * width) && requiredCls.includes(block.event.cls) && block.event.noPass && !ignore.has(block.id) ) { const queue: Block[] = [block]; const arr: [number, number][] = []; // bfs while (queue.length > 0) { const block = queue.shift()!; const { x, y } = block; arr.push([x, y]); for (const [, { x: dx, y: dy }] of Object.entries( core.utils.scan )) { const nx = x + dx; const ny = y + dy; if (nx < 0 || ny < 0 || nx >= width) continue; const loc: LocString = `${nx},${ny}`; const blk = obj[loc]; if (blk) { if ( requiredCls.includes(blk.event.cls) && blk.event.noPass && !used.has(blk.x + blk.y * width) && !ignore.has(blk.id) ) { used.add(blk.x + blk.y * width); queue.push(blk); } } } used.add(x + y * width); } res.push(arr); } } wallCache[floor] = res; return res; } /* @__PURE__ */ export function drawPolygons(floor: FloorIds) { const polygons = calMapPolygons(floor); const ctx = core.createCanvas('polygons', 0, 0, 480, 480, 130); ctx.lineWidth = 1; ctx.lineJoin = 'round'; ctx.strokeStyle = 'white'; for (const p of polygons) { for (const [x, y, w, h] of p) { ctx.strokeRect(x, y, w, h); } } } export class LayerShadowExtends implements ILayerRenderExtends { static shadowList: Set = new Set(); id: string = 'shadow'; hero!: HeroRenderer sprite!: Sprite; private onMoveTick = (x: number, y: number) => { const now = Shadow.now(); if (!now) return; if (now.followHero.size === 0) return; now.followHero.forEach(v => { now.modifyLight(v, { x: x * 32 + 16, y: y * 32 + 16 }); }); now.requestRefresh(); this.sprite.update(this.sprite); } private listen() { this.hero.on('moveTick', this.onMoveTick); } awake(layer: Layer): void { const ex = layer.getExtends('floor-hero'); if (!ex) { layer.removeExtends('shadow'); logger.error(1101, `Shadow extends needs 'floor-hero' extends as dependency.`); return; } this.hero = ex as HeroRenderer; this.listen(); LayerShadowExtends.shadowList.add(this); this.sprite = new Sprite('static', false); this.sprite.setHD(true); this.sprite.size(layer.width, layer.height); this.sprite.setRenderFn((canvas, transform) => { canvas.ctx.drawImage(Shadow.canvas, 0, 0, layer.width, layer.height); }); layer.appendChild(this.sprite); } onDestroy(layer: Layer): void { this.hero.off('moveTick', this.onMoveTick); this.sprite.destroy(); LayerShadowExtends.shadowList.delete(this); } }