Add uniform for lights

This commit is contained in:
schmelczerandras 2020-07-25 16:20:27 +02:00
parent e6782a9a98
commit d336bee1ba
14 changed files with 218 additions and 116 deletions

View file

@ -1,12 +1,14 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
export interface Drawer { export interface Drawer {
startFrame(): void; startFrame(deltaTime: DOMHighResTimeStamp): void;
finishFrame(): void; finishFrame(): void;
giveUniforms(uniforms: any): void; giveUniforms(uniforms: any): void;
appendToUniformList(listName: string, ...values: Array<any>): void;
setCameraPosition(position: vec2): void; setCameraPosition(position: vec2): void;
setCursorPosition(position: vec2): void; setCursorPosition(position: vec2): void;
setInViewArea(size: number): vec2; setInViewArea(size: number): vec2;
screenUvToWorldCoordinate(mousePosition: vec2): vec2;
drawInfoText(text: string): void; drawInfoText(text: string): void;
isOnScreen(position: vec2): boolean; isOnScreen(position: vec2): boolean;
} }

View file

@ -1,4 +1,4 @@
import { mat3, vec2 } from 'gl-matrix'; import { mat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
const loaderMat3 = mat3.create(); const loaderMat3 = mat3.create();
@ -18,10 +18,15 @@ export const loadUniform = (
> = new Map(); > = new Map();
{ {
converters.set(WebGL2RenderingContext.FLOAT, (gl, v, l) => { converters.set(WebGL2RenderingContext.FLOAT, (gl, v, l) => {
if (v.length == 0) { if (v instanceof Array) {
return; if (v.length == 0) {
return;
}
gl.uniform1fv(l, new Float32Array(v));
} else {
gl.uniform1f(l, v);
} }
gl.uniform1fv(l, new Float32Array(v));
}); });
converters.set( converters.set(
@ -39,13 +44,36 @@ export const loadUniform = (
result[2 * i + 1] = (v[i] as Array<number>).y; result[2 * i + 1] = (v[i] as Array<number>).y;
} }
gl.uniform2fv(l, new Float32Array(result)); gl.uniform2fv(l, result);
} else { } else {
gl.uniform2fv(l, v as vec2); gl.uniform2fv(l, v as vec2);
} }
} }
); );
converters.set(
WebGL2RenderingContext.FLOAT_VEC3,
(gl, v: ReadonlyVec3 | Array<vec3>, l) => {
if (v.length == 0) {
return;
}
if (v[0] instanceof Array) {
const result = new Float32Array(v.length * 3);
for (let i = 0; i < v.length; i++) {
result[3 * i] = (v[i] as Array<number>)[0];
result[3 * i + 1] = (v[i] as Array<number>)[1];
result[3 * i + 2] = (v[i] as Array<number>)[2];
}
gl.uniform3fv(l, result);
} else {
gl.uniform3fv(l, v as vec3);
}
}
);
converters.set(WebGL2RenderingContext.FLOAT_MAT3, (gl, v, l) => { converters.set(WebGL2RenderingContext.FLOAT_MAT3, (gl, v, l) => {
gl.uniformMatrix3fv(l, true, mat3.fromMat2d(loaderMat3, v)); gl.uniformMatrix3fv(l, true, mat3.fromMat2d(loaderMat3, v));
}); });
@ -55,7 +83,7 @@ export const loadUniform = (
); );
if (!converters.has(type)) { if (!converters.has(type)) {
throw new Error('Unimplemented webgl type'); throw new Error(`Unimplemented webgl type: ${type}`);
} }
converters.get(type)(gl, value, location); converters.get(type)(gl, value, location);

View file

@ -34,15 +34,15 @@ export class WebGl2Renderer implements Drawer {
new FragmentShaderOnlyProgram(this.gl, ...shaderSources[1]), new FragmentShaderOnlyProgram(this.gl, ...shaderSources[1]),
]); ]);
this.distanceFieldFrameBuffer.renderScale = 0.5; this.distanceFieldFrameBuffer.renderScale = 0.1;
this.lightingFrameBuffer.renderScale = 1; this.lightingFrameBuffer.renderScale = 0.2;
try { try {
this.stopwatch = new WebGlStopwatch(this.gl); this.stopwatch = new WebGlStopwatch(this.gl);
} catch {} } catch {}
} }
public startFrame(): void { public startFrame(deltaTime: DOMHighResTimeStamp): void {
this.stopwatch?.start(); this.stopwatch?.start();
this.uniforms = {}; this.uniforms = {};
this.distanceFieldFrameBuffer.setSize(); this.distanceFieldFrameBuffer.setSize();
@ -76,8 +76,6 @@ export class WebGl2Renderer implements Drawer {
mat2d.scale(ndcToWorld, ndcToWorld, vec2.fromValues(0.5, 0.5)); mat2d.scale(ndcToWorld, ndcToWorld, vec2.fromValues(0.5, 0.5));
mat2d.translate(ndcToWorld, ndcToWorld, vec2.fromValues(1, 1)); mat2d.translate(ndcToWorld, ndcToWorld, vec2.fromValues(1, 1));
const screenToWorld = this.getScreenToWorldTransform(resolution);
const worldToDistanceUV = mat2d.scale( const worldToDistanceUV = mat2d.scale(
mat2d.create(), mat2d.create(),
distanceScreenToWorld, distanceScreenToWorld,
@ -85,11 +83,7 @@ export class WebGl2Renderer implements Drawer {
); );
mat2d.invert(worldToDistanceUV, worldToDistanceUV); mat2d.invert(worldToDistanceUV, worldToDistanceUV);
const cursorPosition = vec2.transformMat2d( const cursorPosition = this.screenUvToWorldCoordinate(this.cursorPosition);
vec2.create(),
vec2.multiply(vec2.create(), this.cursorPosition, resolution),
screenToWorld
);
this.giveUniforms({ this.giveUniforms({
distanceScreenToWorld, distanceScreenToWorld,
@ -115,6 +109,16 @@ export class WebGl2Renderer implements Drawer {
return transform; return transform;
} }
public screenUvToWorldCoordinate(screenUvPosition: vec2): vec2 {
const resolution = vec2.fromValues(this.canvas.width, this.canvas.height);
return vec2.transformMat2d(
vec2.create(),
vec2.multiply(vec2.create(), screenUvPosition, resolution),
this.getScreenToWorldTransform(resolution)
);
}
public setCameraPosition(position: vec2) { public setCameraPosition(position: vec2) {
this.viewBox.topLeft = position; this.viewBox.topLeft = position;
} }
@ -123,6 +127,16 @@ export class WebGl2Renderer implements Drawer {
this.cursorPosition = position; this.cursorPosition = position;
} }
public appendToUniformList(listName: string, ...values: any[]): void {
if (!this.uniforms.hasOwnProperty(listName)) {
this.uniforms[listName] = [];
}
for (let value of values) {
this.uniforms[listName].push(value);
}
}
public giveUniforms(uniforms: any): void { public giveUniforms(uniforms: any): void {
this.uniforms = { ...this.uniforms, ...uniforms }; this.uniforms = { ...this.uniforms, ...uniforms };
} }
@ -144,7 +158,7 @@ export class WebGl2Renderer implements Drawer {
} }
} }
isOnScreen(position: vec2): boolean { public isOnScreen(position: vec2): boolean {
return this.viewBox.isInside(position); return this.viewBox.isInside(position);
} }
} }

View file

@ -13,9 +13,9 @@ import { KeyboardListener } from './input/keyboard-listener';
import { MouseListener } from './input/mouse-listener'; import { MouseListener } from './input/mouse-listener';
import { TouchListener } from './input/touch-listener'; import { TouchListener } from './input/touch-listener';
import { ObjectContainer } from './objects/object-container'; import { ObjectContainer } from './objects/object-container';
import { Character } from './objects/types/character';
import { Dungeon } from './objects/types/dungeon';
import { InfoText } from './objects/types/info-text'; import { InfoText } from './objects/types/info-text';
import { createCharacter } from './objects/world/create-character';
import { createDungeon } from './objects/world/create-dungeon';
export class Game { export class Game {
private previousTime: DOMHighResTimeStamp = 0; private previousTime: DOMHighResTimeStamp = 0;
@ -46,9 +46,9 @@ export class Game {
} }
private initializeScene() { private initializeScene() {
this.objects.addObject(new Character(this.objects));
this.objects.addObject(new InfoText()); this.objects.addObject(new InfoText());
this.objects.addObject(new Dungeon()); createCharacter(this.objects);
createDungeon(this.objects);
} }
@timeIt() @timeIt()
@ -59,7 +59,7 @@ export class Game {
this.objects.sendCommand(new StepCommand(deltaTime)); this.objects.sendCommand(new StepCommand(deltaTime));
this.renderer.startFrame(); this.renderer.startFrame(deltaTime);
this.objects.sendCommand(new BeforeDrawCommand(this.renderer)); this.objects.sendCommand(new BeforeDrawCommand(this.renderer));
this.objects.sendCommand(new DrawCommand(this.renderer)); this.objects.sendCommand(new DrawCommand(this.renderer));
this.renderer.finishFrame(); this.renderer.finishFrame();

View file

@ -28,6 +28,10 @@ export abstract class GameObject extends Typed implements CommandReceiver {
this.commandExecutors[commandType.name] = handler; this.commandExecutors[commandType.name] = handler;
} }
public reactsToCommand<T extends Command>(commandType: new () => T): boolean {
return this.commandExecutors.hasOwnProperty(commandType.name);
}
public sendCommand(command: Command) { public sendCommand(command: Command) {
const commandType = command.constructor.name; const commandType = command.constructor.name;

View file

@ -5,19 +5,16 @@ import { MoveToCommand } from '../../commands/types/move-to';
import { StepCommand } from '../../commands/types/step'; import { StepCommand } from '../../commands/types/step';
import { SwipeCommand } from '../../commands/types/swipe'; import { SwipeCommand } from '../../commands/types/swipe';
import { GameObject } from '../game-object'; import { GameObject } from '../game-object';
import { ObjectContainer } from '../object-container';
import { Camera } from './camera'; import { Camera } from './camera';
export class Character extends GameObject { export class Character extends GameObject {
private keysDown: Set<string> = new Set(); private keysDown: Set<string> = new Set();
private camera = new Camera();
private static speed = 0.5; private static speed = 0.5;
constructor(objects: ObjectContainer) { constructor(private camera: Camera) {
super(); super();
objects.addObject(this.camera);
this.addCommandExecutor(StepCommand, this.stepHandler.bind(this)); this.addCommandExecutor(StepCommand, this.stepHandler.bind(this));
this.addCommandExecutor(KeyDownCommand, (c) => this.keysDown.add(c.key)); this.addCommandExecutor(KeyDownCommand, (c) => this.keysDown.add(c.key));
this.addCommandExecutor(KeyUpCommand, (c) => this.keysDown.delete(c.key)); this.addCommandExecutor(KeyUpCommand, (c) => this.keysDown.delete(c.key));

View file

@ -0,0 +1,25 @@
import { vec2, vec3 } from 'gl-matrix';
import { DrawCommand } from '../../commands/types/draw';
import { GameObject } from '../game-object';
export class CircleLight extends GameObject {
constructor(
private center: vec2,
private radius: number,
private value: vec3
) {
super();
this.addCommandExecutor(DrawCommand, this.draw.bind(this));
}
private draw(c: DrawCommand) {
if (c.drawer.isOnScreen(this.center)) {
c.drawer.appendToUniformList('lights', {
center: this.center,
radius: this.radius,
value: this.value,
});
}
}
}

View file

@ -0,0 +1,30 @@
import { vec2, vec3 } from 'gl-matrix';
import { CursorMoveCommand } from '../../commands/types/cursor-move-command';
import { DrawCommand } from '../../commands/types/draw';
import { GameObject } from '../game-object';
export class CursorLight extends GameObject {
private mousePosition = vec2.create();
constructor(private radius: number, private value: vec3) {
super();
this.addCommandExecutor(DrawCommand, this.draw.bind(this));
this.addCommandExecutor(CursorMoveCommand, this.setPosition.bind(this));
}
private draw(c: DrawCommand) {
const center = c.drawer.screenUvToWorldCoordinate(this.mousePosition);
if (c.drawer.isOnScreen(center)) {
c.drawer.appendToUniformList('lights', {
center,
radius: this.radius,
value: this.value,
});
}
}
private setPosition(c: CursorMoveCommand) {
this.mousePosition = c.position;
}
}

View file

@ -1,55 +0,0 @@
import { vec2 } from 'gl-matrix';
import { DrawCommand } from '../../commands/types/draw';
import { GameObject } from '../game-object';
export interface Line {
start: vec2;
end: vec2;
radiusFrom: number;
radiusTo: number;
}
export class Dungeon extends GameObject {
private lines: Array<Line> = [];
constructor() {
super();
this.addCommandExecutor(DrawCommand, this.draw.bind(this));
let previousRadius = 350;
let previousEnd = vec2.create();
for (let i = 0; i < 500000; i += 500) {
const height = previousEnd.y + (Math.random() - 0.5) * 2000;
const currentEnd = vec2.fromValues(i, height);
const currentToRadius = Math.random() * 300 + 150;
this.lines.push({
start: previousEnd,
end: currentEnd,
radiusFrom: previousRadius,
radiusTo: currentToRadius,
});
previousEnd = currentEnd;
previousRadius = currentToRadius;
}
}
private draw(c: DrawCommand) {
const lines: Array<vec2> = [];
const radii: Array<number> = [];
for (let line of this.lines) {
if (c.drawer.isOnScreen(line.start) || c.drawer.isOnScreen(line.end)) {
lines.push(line.start);
lines.push(line.end);
radii.push(line.radiusFrom);
radii.push(line.radiusTo);
}
}
c.drawer.giveUniforms({ lines, radii });
}
}

View file

@ -0,0 +1,25 @@
import { vec2 } from 'gl-matrix';
import { DrawCommand } from '../../commands/types/draw';
import { GameObject } from '../game-object';
export interface Line {}
export class Tunnel extends GameObject {
constructor(
private from: vec2,
private to: vec2,
private radiusFrom: number,
private radiusTo: number
) {
super();
this.addCommandExecutor(DrawCommand, this.draw.bind(this));
}
private draw(c: DrawCommand) {
if (c.drawer.isOnScreen(this.from) || c.drawer.isOnScreen(this.to)) {
c.drawer.appendToUniformList('lines', this.from, this.to);
c.drawer.appendToUniformList('radii', this.radiusFrom, this.radiusTo);
}
}
}

View file

@ -0,0 +1,12 @@
import { vec3 } from 'gl-matrix';
import { ObjectContainer } from '../object-container';
import { Camera } from '../types/camera';
import { Character } from '../types/character';
import { CursorLight } from '../types/cursor-light';
export const createCharacter = (objects: ObjectContainer) => {
const camera = new Camera();
objects.addObject(camera);
objects.addObject(new Character(camera));
objects.addObject(new CursorLight(40, vec3.fromValues(0.67, 0.67, 0.33)));
};

View file

@ -0,0 +1,37 @@
import { vec2, vec3 } from 'gl-matrix';
import { ObjectContainer } from '../object-container';
import { CircleLight } from '../types/circle-light';
import { Tunnel } from '../types/tunnel';
export const createDungeon = (objects: ObjectContainer) => {
let previousRadius = 350;
let previousEnd = vec2.create();
for (let i = 0; i < 500000; i += 500) {
const deltaHeight = (Math.random() - 0.5) * 2000;
const height = previousEnd.y + deltaHeight;
const currentEnd = vec2.fromValues(i, height);
const currentToRadius = Math.random() * 300 + 150;
objects.addObject(
new Tunnel(previousEnd, currentEnd, previousRadius, currentToRadius)
);
if (deltaHeight > 0) {
objects.addObject(
new CircleLight(
currentEnd,
Math.random() * 20 + 30,
vec3.scale(
vec3.create(),
vec3.normalize(vec3.create(), vec3.random(vec3.create())),
Math.random() * 0.5 + 0.5
)
)
);
}
previousEnd = currentEnd;
previousRadius = currentToRadius;
}
};

View file

@ -1,12 +1,10 @@
#version 300 es #version 300 es
precision lowp float; precision mediump float;
#define INFINITY 200.0
#define INFINITY 10000.0
#define LINE_COUNT 50 #define LINE_COUNT 50
uniform vec2[LINE_COUNT * 2] lines; uniform vec2[LINE_COUNT * 2] lines;
uniform float[LINE_COUNT * 2] radii; uniform float[LINE_COUNT * 2] radii;
@ -18,8 +16,9 @@ float lineDistance(
in float radiusTo in float radiusTo
) { ) {
vec2 pa = target - start, ba = end - start; vec2 pa = target - start, ba = end - start;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); float baLength = length(ba);
return distance(pa, ba * h) - mix(radiusFrom, radiusTo, h); float h = clamp(dot(pa / baLength, ba / baLength), 0.0, 1.0);
return length(pa - ba * h) - mix(radiusFrom, radiusTo, h);
} }
float getDistance(in vec2 target) { float getDistance(in vec2 target) {

View file

@ -3,20 +3,18 @@
precision mediump float; precision mediump float;
#define INFINITY 10000.0 #define INFINITY 10000.0
#define LIGHT_COUNT 3 #define LIGHT_COUNT 5
#define AMBIENT_LIGHT vec3(0.05) #define AMBIENT_LIGHT vec3(0.05)
#define LIGHT_DROP 800.0 #define LIGHT_DROP 800.0
#define SHADOW_BIAS 0.01 #define SHADOW_BIAS 0.01
struct Light { uniform struct Light {
vec2 center; vec2 center;
float radius; float radius;
vec3 value; vec3 value;
}[LIGHT_COUNT] lights; }[LIGHT_COUNT] lights;
uniform sampler2D distanceTexture; uniform sampler2D distanceTexture;
uniform mat3 worldToDistanceUV;
uniform vec2 cursorPosition;
uniform vec2 viewBoxSize; uniform vec2 viewBoxSize;
float square(in float a) { float square(in float a) {
@ -24,26 +22,13 @@ float square(in float a) {
} }
float getDistance(in vec2 target, out vec3 color) { float getDistance(in vec2 target, out vec3 color) {
// should avoid this matrix multiplication vec4 values = texture(distanceTexture, target);
vec2 targetUV = (vec3(target.xy, 1.0) * worldToDistanceUV).xy;
vec4 values = texture(distanceTexture, targetUV);
color = values.rgb; color = values.rgb;
return values.w * 32.0; return values.w * 32.0;
} }
float getDistance(in vec2 target) { float getDistance(in vec2 target) {
// should avoid this matrix multiplication return texture(distanceTexture, target).w * 32.0;
vec2 targetUV = (vec3(target.xy, 1.0) * worldToDistanceUV).xy;
vec4 values = texture(distanceTexture, targetUV);
return values.w * 32.0;
}
void createWorld() {
//lights[0] = Light(vec2(600, 700), 40.5, normalize(vec3(1.0)) * 2.0);
//lights[1] = Light(vec2(100.0, 350.0), 52.5, normalize(vec3(2.0, 1.0, 0.25)) * 0.5);
lights[2] = Light(cursorPosition, 52.5, normalize(vec3(0.93, 0.25, 0.5)) * 1.0);
} }
float getFractionOfLightArriving( float getFractionOfLightArriving(
@ -53,23 +38,25 @@ float getFractionOfLightArriving(
in float lightDistance, in float lightDistance,
in float lightRadius in float lightRadius
) { ) {
float q = INFINITY; float q = 1.0;
float rayLength = 0.0; float rayLength = startingDistance;
float movingAverageMeanDistance = startingDistance; float movingAverageMeanDistance = startingDistance;
direction /= viewBoxSize;
for (int j = 0; j < 64; j++) { for (int j = 0; j < 64; j++) {
float minDistance = getDistance(target + direction * rayLength); float minDistance = getDistance(target + direction * rayLength);
movingAverageMeanDistance = movingAverageMeanDistance / 2.0 + minDistance / 2.0; movingAverageMeanDistance = movingAverageMeanDistance / 2.0 + minDistance / 2.0;
q = min(q, movingAverageMeanDistance / rayLength); q = min(q, movingAverageMeanDistance / rayLength);
rayLength = min(lightDistance, rayLength + max(0.0001, minDistance)); rayLength = min(lightDistance, rayLength + max(1.0, minDistance));
} }
return smoothstep(0.0, 1.0, (q - SHADOW_BIAS) * (lightDistance + lightRadius) / lightRadius); return smoothstep(0.0, 1.0, (q - SHADOW_BIAS) * (lightDistance + lightRadius) / lightRadius);
} }
vec3 getPixelColor(in vec2 worldCoordinates, in vec2 uvCoordinates) { vec3 getPixelColor(in vec2 worldCoordinates, in vec2 uvCoordinates) {
vec3 colorAtPosition; vec3 colorAtPosition;
float startingDistance = getDistance(worldCoordinates, colorAtPosition); float startingDistance = getDistance(uvCoordinates, colorAtPosition);
vec3 result = colorAtPosition * AMBIENT_LIGHT; vec3 result = colorAtPosition * AMBIENT_LIGHT;
@ -81,7 +68,7 @@ vec3 getPixelColor(in vec2 worldCoordinates, in vec2 uvCoordinates) {
vec2 lightDirection = normalize(light.center - worldCoordinates); vec2 lightDirection = normalize(light.center - worldCoordinates);
float fractionOfLightArriving = getFractionOfLightArriving( float fractionOfLightArriving = getFractionOfLightArriving(
worldCoordinates, lightDirection, startingDistance, uvCoordinates, lightDirection, startingDistance,
max(0.0, lightDistance), light.radius max(0.0, lightDistance), light.radius
); );
@ -91,13 +78,10 @@ vec3 getPixelColor(in vec2 worldCoordinates, in vec2 uvCoordinates) {
return clamp(result, 0.0, 1.0); return clamp(result, 0.0, 1.0);
} }
in vec2 worldCoordinates; in vec2 worldCoordinates;
in vec2 uvCoordinates; in vec2 uvCoordinates;
out vec4 fragmentColor; out vec4 fragmentColor;
void main() { void main() {
createWorld();
fragmentColor = vec4(getPixelColor(worldCoordinates, uvCoordinates), 1.0); fragmentColor = vec4(getPixelColor(worldCoordinates, uvCoordinates), 1.0);
} }