HumanBreak/src/core/fx/shadow.ts
2024-08-27 00:56:48 +08:00

1363 lines
45 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { mat4 } from 'gl-matrix';
import { logger } from '../common/logger';
import {
WebGLColorArray,
createProgram,
isWebGL2Supported
} from './webgl';
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<AllNumbers> = 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.2] },
{ 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.3], noShelter: true },
{ background: [0, 0, 0, 0.3] }
);
// 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.update());
})
hook.on('setBlock', () => {
Shadow.update(true);
LayerShadowExtends.shadowList.forEach(v => v.update());
})
hook.on('changingFloor', floorId => {
Shadow.clearBuffer();
Shadow.update();
// setCanvasFilterByFloorId(floorId);
LayerShadowExtends.shadowList.forEach(v => v.update());
})
// 深度测试着色器
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<Record<FloorIds, Shadow>> = {};
private static locations: ShadowLocations;
private static martix: ShadowMatrix;
private static buffer: ShadowBuffer;
private static texture: ShadowTexture;
private static cached: Set<FloorIds> = new Set();
floorId: FloorIds;
lights: LightInfo[] = [];
immerse: number = 4;
blur: number = 4;
background: WebGLColorArray = [0, 0, 0, 0];
originLightInfo: Record<string, LightInfo> = {};
followHero: Set<string> = 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<LightInfo>) {
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<Record<FloorIds, [number, number][][]>> = {};
const polygonCache: Partial<
Record<FloorIds, [number, number, number, number][][]>
> = {};
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<number> = 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<number> = 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<LayerShadowExtends> = new Set();
id: string = 'shadow';
layer!: Layer
hero!: HeroRenderer
sprite!: Sprite;
update() {
this.sprite.update(this.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.layer.requestAfterFrame(() => {
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 instanceof HeroRenderer)) {
layer.removeExtends('shadow');
logger.error(1101, `Shadow extends needs 'floor-hero' extends as dependency.`);
return;
}
this.hero = ex;
this.layer = layer;
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);
}
}