mirror of
https://github.com/Heretek-AI/GDevelop-Heretek.git
synced 2026-10-12 02:02:42 +00:00
Fix 2D lights with a texture not being able to be rotated (#9025)
Co-authored-by: Mykyta Bozhdohan <[email protected]>
This commit is contained in:
co-authored by
Mykyta Bozhdohan
parent
8d0563f9ed
commit
4efcefb190
@@ -72,6 +72,9 @@ namespace gdjs {
|
||||
this.updateTexture();
|
||||
this._center = new Float32Array([runtimeObject.x, runtimeObject.y]);
|
||||
this._defaultVertexBuffer = new Float32Array(8);
|
||||
// The object's angle is not applied yet at construction time (it is set
|
||||
// afterwards, via setAngle), so the initial quad is the axis-aligned
|
||||
// square. _updateBuffers rebuilds it with the rotation applied.
|
||||
this._vertexBuffer = new Float32Array([
|
||||
runtimeObject.x - this._radius,
|
||||
runtimeObject.y + this._radius,
|
||||
@@ -326,6 +329,7 @@ namespace gdjs {
|
||||
center: this._center,
|
||||
radius: this._radius,
|
||||
color: this._color,
|
||||
angle: gdjs.toRad(this._object.getAngle()),
|
||||
};
|
||||
if (this._texture) {
|
||||
// @ts-ignore
|
||||
@@ -409,21 +413,24 @@ namespace gdjs {
|
||||
|
||||
const computedVertices = this._computeLightVertices();
|
||||
if (!computedVertices.length) {
|
||||
// Draw the same rotated quad the mesh falls back to when there are
|
||||
// no obstacles around (see _updateBuffers), so the debug overlay
|
||||
// matches the actual light boundary. _updateBuffers recomputes the
|
||||
// same values into this buffer right after, so reusing it is safe.
|
||||
const corners = this._defaultVertexBuffer;
|
||||
this._computeRotatedSquareVertices(
|
||||
gdjs.toRad(this._object.getAngle()),
|
||||
corners
|
||||
);
|
||||
debugGraphics.clear();
|
||||
debugGraphics
|
||||
.lineStyle(1, 16711680, 1)
|
||||
.moveTo(this._object.x, this._object.y)
|
||||
.lineTo(this._object.x - this._radius, this._object.y + this._radius)
|
||||
.lineTo(this._object.x + this._radius, this._object.y + this._radius)
|
||||
.moveTo(this._object.x, this._object.y)
|
||||
.lineTo(this._object.x + this._radius, this._object.y + this._radius)
|
||||
.lineTo(this._object.x + this._radius, this._object.y - this._radius)
|
||||
.moveTo(this._object.x, this._object.y)
|
||||
.lineTo(this._object.x + this._radius, this._object.y - this._radius)
|
||||
.lineTo(this._object.x - this._radius, this._object.y - this._radius)
|
||||
.moveTo(this._object.x, this._object.y)
|
||||
.lineTo(this._object.x - this._radius, this._object.y - this._radius)
|
||||
.lineTo(this._object.x - this._radius, this._object.y + this._radius);
|
||||
debugGraphics.lineStyle(1, 16711680, 1);
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
const next = (i + 2) % 8;
|
||||
debugGraphics
|
||||
.moveTo(this._object.x, this._object.y)
|
||||
.lineTo(corners[i], corners[i + 1])
|
||||
.lineTo(corners[next], corners[next + 1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const vertices = new Array(2 * computedVertices.length + 2);
|
||||
@@ -451,24 +458,46 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the given buffer (4 corners, as x/y pairs) with the corners of the
|
||||
* light's square *rotated by the given angle* — the same rotated square
|
||||
* the shader samples the texture from (see texturedFragmentShader). At
|
||||
* angle 0, this is exactly the axis-aligned square
|
||||
* [x-radius, x+radius] x [y-radius, y+radius].
|
||||
*/
|
||||
_computeRotatedSquareVertices(angleRad: float, output: Float32Array) {
|
||||
const cosA = Math.cos(angleRad);
|
||||
const sinA = Math.sin(angleRad);
|
||||
const cx = this._object.x;
|
||||
const cy = this._object.y;
|
||||
const a = this._radius * (cosA + sinA);
|
||||
const b = this._radius * (cosA - sinA);
|
||||
output[0] = cx - a;
|
||||
output[1] = cy + b;
|
||||
output[2] = cx + b;
|
||||
output[3] = cy + a;
|
||||
output[4] = cx + a;
|
||||
output[5] = cy - b;
|
||||
output[6] = cx - b;
|
||||
output[7] = cy - a;
|
||||
}
|
||||
|
||||
_updateBuffers() {
|
||||
if (!this._light) {
|
||||
return;
|
||||
}
|
||||
this._center[0] = this._object.x;
|
||||
this._center[1] = this._object.y;
|
||||
const angleRad = gdjs.toRad(this._object.getAngle());
|
||||
this._light.shader.uniforms.angle = angleRad;
|
||||
const vertices = this._computeLightVertices();
|
||||
|
||||
// Fallback to simple quad when there are no obstacles around.
|
||||
// Fallback to simple quad when there are no obstacles around: the
|
||||
// light's rotated square, so it exactly matches the area the shader
|
||||
// samples the texture from, without clipping its corners nor
|
||||
// over-sizing the mesh.
|
||||
if (vertices.length === 0) {
|
||||
this._defaultVertexBuffer[0] = this._object.x - this._radius;
|
||||
this._defaultVertexBuffer[1] = this._object.y + this._radius;
|
||||
this._defaultVertexBuffer[2] = this._object.x + this._radius;
|
||||
this._defaultVertexBuffer[3] = this._object.y + this._radius;
|
||||
this._defaultVertexBuffer[4] = this._object.x + this._radius;
|
||||
this._defaultVertexBuffer[5] = this._object.y - this._radius;
|
||||
this._defaultVertexBuffer[6] = this._object.x - this._radius;
|
||||
this._defaultVertexBuffer[7] = this._object.y - this._radius;
|
||||
this._computeRotatedSquareVertices(angleRad, this._defaultVertexBuffer);
|
||||
this._light.shader.uniforms.center = this._center;
|
||||
this._light.geometry
|
||||
.getBuffer('aVertexPosition')
|
||||
@@ -546,18 +575,35 @@ namespace gdjs {
|
||||
* @returns the vertices of mesh.
|
||||
*/
|
||||
_computeLightVertices(): Array<FloatPoint> {
|
||||
// An untextured light renders as a circle (see defaultFragmentShader),
|
||||
// which never lights anything beyond ±radius whatever the angle, so
|
||||
// the ±radius box is an exact obstacle search area for it.
|
||||
// A textured light's lit region is the light's square *rotated by the
|
||||
// object's angle* (see the fallback quad in _updateBuffers and
|
||||
// texturedFragmentShader), whose axis-aligned bounding box extends up
|
||||
// to radius * (|cos| + |sin|) from the center. Search obstacles in
|
||||
// that whole box — searching only ±radius would miss obstacles near
|
||||
// the rotated square's corners and let the light shine through them.
|
||||
// At angle 0 both are exactly ±radius, as before rotation support.
|
||||
let searchHalfExtent = this._radius;
|
||||
if (this._texture !== null) {
|
||||
const angleRad = gdjs.toRad(this._object.getAngle());
|
||||
searchHalfExtent =
|
||||
this._radius *
|
||||
(Math.abs(Math.cos(angleRad)) + Math.abs(Math.sin(angleRad)));
|
||||
}
|
||||
const lightObstacles = this._lightObstaclesTemp;
|
||||
if (this._manager) {
|
||||
this._manager.getAllObstaclesAround(
|
||||
this._object,
|
||||
this._radius,
|
||||
searchHalfExtent,
|
||||
lightObstacles
|
||||
);
|
||||
}
|
||||
const searchAreaLeft = this._object.getX() - this._radius;
|
||||
const searchAreaTop = this._object.getY() - this._radius;
|
||||
const searchAreaRight = this._object.getX() + this._radius;
|
||||
const searchAreaBottom = this._object.getY() + this._radius;
|
||||
const searchAreaLeft = this._object.getX() - searchHalfExtent;
|
||||
const searchAreaTop = this._object.getY() - searchHalfExtent;
|
||||
const searchAreaRight = this._object.getX() + searchHalfExtent;
|
||||
const searchAreaBottom = this._object.getY() + searchHalfExtent;
|
||||
|
||||
// Bail out early if there are no obstacles.
|
||||
if (lightObstacles.length === 0) {
|
||||
@@ -590,10 +636,16 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
let maxX = this._object.x + this._radius;
|
||||
let minX = this._object.x - this._radius;
|
||||
let maxY = this._object.y + this._radius;
|
||||
let minY = this._object.y - this._radius;
|
||||
// Seed the self-boundary ("wall" that unoccluded rays terminate on)
|
||||
// with the axis-aligned bounding box of the light's rotated square —
|
||||
// the same box used to search obstacles above. At angle 0 this is
|
||||
// exactly [x-radius, x+radius] x [y-radius, y+radius], same as before;
|
||||
// at other angles it grows just enough to avoid clipping the corners
|
||||
// of the rotated texture, without over-sizing the search area.
|
||||
let minX = searchAreaLeft;
|
||||
let maxX = searchAreaRight;
|
||||
let minY = searchAreaTop;
|
||||
let maxY = searchAreaBottom;
|
||||
const flattenVertices = this._flattenVerticesTemp;
|
||||
flattenVertices.length = 0;
|
||||
for (let i = 1; i < obstaclePolygons.length; i++) {
|
||||
@@ -776,12 +828,22 @@ namespace gdjs {
|
||||
uniform vec2 center;
|
||||
uniform float radius;
|
||||
uniform vec3 color;
|
||||
uniform float angle;
|
||||
uniform sampler2D uSampler;
|
||||
varying vec2 vPos;
|
||||
|
||||
void main() {
|
||||
vec2 topleft = vec2(center.x - radius, center.y - radius);
|
||||
vec2 texCoord = (vPos - topleft)/(2.0 * radius);
|
||||
vec2 relativePos = vPos - center;
|
||||
float cosA = cos(angle);
|
||||
float sinA = sin(angle);
|
||||
// Rotate the sampled point back by the object's angle so that the
|
||||
// texture appears to rotate together with the light object.
|
||||
vec2 rotatedPos = vec2(
|
||||
relativePos.x * cosA + relativePos.y * sinA,
|
||||
-relativePos.x * sinA + relativePos.y * cosA
|
||||
);
|
||||
vec2 topleft = vec2(-radius, -radius);
|
||||
vec2 texCoord = (rotatedPos - topleft)/(2.0 * radius);
|
||||
gl_FragColor = (texCoord.x > 0.0 && texCoord.x < 1.0 && texCoord.y > 0.0 && texCoord.y < 1.0)
|
||||
? vec4(color, 1.0) * texture2D(uSampler, texCoord)
|
||||
: vec4(0.0, 0.0, 0.0, 0.0);
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* Utility function for adding light object for tests.
|
||||
* @param {gdjs.RuntimeScene} runtimeScene
|
||||
* @param {number} radius
|
||||
* @param {string} [texture] Texture resource name ('' for an untextured light).
|
||||
* @returns {gdjs.LightRuntimeObject}
|
||||
*/
|
||||
const addLightObject = (runtimeScene, radius) => {
|
||||
const addLightObject = (runtimeScene, radius, texture = '') => {
|
||||
const lightObj = new gdjs.LightRuntimeObject(runtimeScene, {
|
||||
name: 'lightObject',
|
||||
type: 'Lighting::LightObject',
|
||||
@@ -18,7 +19,7 @@ const addLightObject = (runtimeScene, radius) => {
|
||||
content: {
|
||||
radius: radius,
|
||||
color: '#b4b4b4',
|
||||
texture: '',
|
||||
texture: texture,
|
||||
debugMode: false,
|
||||
},
|
||||
});
|
||||
@@ -193,4 +194,44 @@ describe('Light with obstacles around it', function () {
|
||||
expect(indexBuffer[index]).to.be(val);
|
||||
});
|
||||
});
|
||||
|
||||
it('Rotated untextured light ignores obstacles beyond its circular radius.', function () {
|
||||
// An untextured light renders as a circle that never lights anything
|
||||
// beyond ±radius whatever the angle, so an obstacle outside the
|
||||
// ±radius box ([100, 300] x [100, 300]) must not trigger raycasting.
|
||||
light.setPosition(200, 200);
|
||||
light.setAngle(45);
|
||||
obstacle.setPosition(320, 175);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
light.update();
|
||||
|
||||
expect(light._renderer._computeLightVertices().length).to.be(0);
|
||||
});
|
||||
|
||||
it('Rotated textured light is occluded by an obstacle beyond the unrotated radius box.', function () {
|
||||
// A textured light's lit area is the light's square rotated by the
|
||||
// object's angle: at 45°, it extends up to radius * sqrt(2) ≈ 141.42
|
||||
// from the center along the axes. Place the obstacle outside the
|
||||
// unrotated ±radius box ([100, 300] x [100, 300]) but inside the lit
|
||||
// area: it must still be found and occlude the light.
|
||||
const texturedLight = addLightObject(runtimeScene, 100, 'texture.png');
|
||||
texturedLight.setPosition(200, 200);
|
||||
texturedLight.setAngle(45);
|
||||
obstacle.setPosition(320, 175);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
texturedLight.update();
|
||||
|
||||
const vertices = texturedLight._renderer._computeLightVertices();
|
||||
expect(vertices.length).to.be.greaterThan(0);
|
||||
// Rays cast towards the obstacle stop on its left edge (x = 320)
|
||||
// instead of reaching the boundary of the lit area.
|
||||
expect(
|
||||
vertices.some(
|
||||
(vertex) =>
|
||||
Math.abs(vertex[0] - 320) < 0.5 &&
|
||||
vertex[1] >= 175 &&
|
||||
vertex[1] <= 225
|
||||
)
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user