├── .gitignore
├── shader_minifier.exe
├── externs.js
├── src
├── core
│ ├── state.ts
│ ├── state-machine.ts
│ ├── controls.ts
│ └── first-person-player.ts
├── vite-game.d.ts
├── engine
│ ├── shaders
│ │ ├── depth.fragment.glsl
│ │ ├── skybox.vertex.glsl
│ │ ├── depth.vertex.glsl
│ │ ├── skybox.fragment.glsl
│ │ ├── vertex.glsl
│ │ ├── fragment.glsl
│ │ └── shaders.ts
│ ├── renderer
│ │ ├── texture.ts
│ │ ├── mesh.ts
│ │ ├── material.ts
│ │ ├── camera.ts
│ │ ├── texture-loader.ts
│ │ ├── scene.ts
│ │ ├── lil-gl.ts
│ │ ├── object-3d.ts
│ │ └── renderer.ts
│ ├── plane-geometry.ts
│ ├── physics
│ │ ├── face.ts
│ │ ├── parse-faces.ts
│ │ └── surface-collision.ts
│ ├── skybox.ts
│ ├── new-new-noise.ts
│ ├── svg-maker
│ │ ├── converters.ts
│ │ └── base.ts
│ ├── helpers.ts
│ ├── audio
│ │ └── audio-player.ts
│ ├── enhanced-dom-point.ts
│ └── moldable-cube-geometry.ts
├── game-states
│ ├── game-states.ts
│ ├── menu.state.ts
│ └── game.state.ts
├── game-state-machine.ts
├── draw-helpers.ts
├── index.ts
├── sound-effects.ts
├── modeling
│ ├── building-blocks.ts
│ ├── lever-door.ts
│ ├── items.ts
│ └── castle.ts
└── textures.ts
├── index.html
├── README.md
├── style.css
├── package.json
├── .eslintrc.js
├── find-best-roadroller.js
├── tsconfig.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project exclude paths
2 | /dist/
3 | /node_modules/
--------------------------------------------------------------------------------
/shader_minifier.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roblouie/upyri/HEAD/shader_minifier.exe
--------------------------------------------------------------------------------
/externs.js:
--------------------------------------------------------------------------------
1 | let localStorage;
2 | let c3d;
3 | let Start;
4 | let tmpl;
5 | let Fullscreen;
6 |
--------------------------------------------------------------------------------
/src/core/state.ts:
--------------------------------------------------------------------------------
1 | export interface State {
2 | onUpdate: (timeElapsed: number) => void;
3 | onEnter?: Function;
4 | onLeave?: Function;
5 | }
--------------------------------------------------------------------------------
/src/vite-game.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.jpg';
2 | declare module '*.png';
3 |
4 | // HTML Element Ids
5 | declare const tmpl;
6 | declare const c3d;
7 | declare const Fullscreen;
8 | declare const Start;
9 |
--------------------------------------------------------------------------------
/src/engine/shaders/depth.fragment.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | //[
4 | precision highp float;
5 | //]
6 |
7 | out float fragDepth;
8 |
9 | void main(){
10 | fragDepth = gl_FragCoord.z;
11 | }
12 |
--------------------------------------------------------------------------------
/src/game-states/game-states.ts:
--------------------------------------------------------------------------------
1 | import { State } from '@/engine/state-machine/state';
2 |
3 | export const gameStates = {
4 | gameState: {} as State,
5 | menuState: {} as State,
6 | levelOverState: {} as State,
7 | };
8 |
--------------------------------------------------------------------------------
/src/engine/shaders/skybox.vertex.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | layout(location = 0) in vec4 aCoords;
4 | out vec4 v_position;
5 |
6 | void main() {
7 | v_position = aCoords;
8 | gl_Position = aCoords;
9 | gl_Position.z = 1.0;
10 | }
11 |
--------------------------------------------------------------------------------
/src/engine/shaders/depth.vertex.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | //[
4 | precision highp float;
5 | //]
6 |
7 | layout(location=0) in vec4 aPosition;
8 |
9 | uniform mat4 lightPovMvp;
10 |
11 | void main(){
12 | gl_Position = lightPovMvp * aPosition;
13 | }
14 |
--------------------------------------------------------------------------------
/src/game-state-machine.ts:
--------------------------------------------------------------------------------
1 | import { StateMachine } from './core/state-machine';
2 | import { State } from './core/state';
3 |
4 | export let gameStateMachine: StateMachine;
5 |
6 | export function createGameStateMachine(initialState: State, ...initialArguments: any[]) {
7 | gameStateMachine = new StateMachine(initialState, ...initialArguments);
8 | }
9 |
--------------------------------------------------------------------------------
/src/engine/renderer/texture.ts:
--------------------------------------------------------------------------------
1 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point";
2 |
3 | export class Texture {
4 | id: number;
5 | source: TexImageSource;
6 | textureRepeat = new EnhancedDOMPoint(1, 1);
7 |
8 | constructor(id: number, source: TexImageSource) {
9 | this.source = source;
10 | this.id = id;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | UPYRI
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/engine/shaders/skybox.fragment.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | //[
3 | precision highp float;
4 | //]
5 |
6 | uniform samplerCube u_skybox;
7 | uniform mat4 u_viewDirectionProjectionInverse;
8 |
9 | in vec4 v_position;
10 |
11 | out vec4 outColor;
12 |
13 | void main() {
14 | vec4 t = u_viewDirectionProjectionInverse * v_position;
15 | outColor = texture(u_skybox, t.xyz);
16 | }
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UPYRI
2 |
3 | My entry for the 2023 JS13K competition. The game placed 5th overall.
4 |
5 | ## Trailer
6 | [](http://www.youtube.com/watch?v=sATZin4rFwQ "UPYRI")
7 |
8 | ## Making Of / Post Mortem
9 | https://roblouie.com/article/1154/the-making-of-upyri-js13k-2023-post-mortem/
10 |
11 | ## Quick Start
12 |
13 | Install dependencies: `npm install`
14 |
15 | Run Server: `npm run serve`
16 |
--------------------------------------------------------------------------------
/src/engine/renderer/mesh.ts:
--------------------------------------------------------------------------------
1 | import { Object3d } from './object-3d';
2 | import { Material } from './material';
3 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry';
4 |
5 | export class Mesh extends Object3d {
6 | geometry: MoldableCubeGeometry;
7 | material: Material;
8 |
9 | constructor(geometry: MoldableCubeGeometry, material: Material) {
10 | super();
11 | this.geometry = geometry;
12 | this.material = material;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/engine/renderer/material.ts:
--------------------------------------------------------------------------------
1 | import { Texture } from '@/engine/renderer/texture';
2 |
3 | export class Material {
4 | emissive = [0.0, 0.0, 0.0, 0.0];
5 | texture?: Texture;
6 | isTransparent = false;
7 |
8 | constructor(props?: { texture?: Texture, emissive?: [number, number, number, number], isTransparent?: boolean }) {
9 | this.texture = props?.texture;
10 | this.emissive = props?.emissive ? props.emissive : this.emissive;
11 | this.isTransparent = props?.isTransparent ?? this.isTransparent;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | canvas, svg, #tmpl {
2 | width: min(100vw, calc(100vh * 16/9));
3 | position: absolute;
4 | aspect-ratio: 16/9;
5 | }
6 |
7 | html, body {
8 | margin: 0;
9 | background-color: black;
10 | display: grid;
11 | place-items: center;
12 | height: 100%;
13 | }
14 |
15 | svg {
16 | font-family: sans-serif;
17 | font-size: 80px;
18 | font-weight: bold;
19 | }
20 |
21 | #Fullscreen,#Start {
22 | fill: #666;
23 | cursor: pointer;
24 | }
25 |
26 | #Fullscreen:hover,#Start:hover {
27 | fill: red;
28 | }
29 |
--------------------------------------------------------------------------------
/src/engine/plane-geometry.ts:
--------------------------------------------------------------------------------
1 | import { MoldableCubeGeometry } from "@/engine/moldable-cube-geometry";
2 |
3 | export class PlaneGeometry extends MoldableCubeGeometry {
4 |
5 | constructor(width_ = 1, depth = 1, subdivisionsWidth = 1, subdivisionsDepth = 1, heightmap?: number[]) {
6 | super(width_, 1, depth, subdivisionsWidth, 0, subdivisionsDepth, 1);
7 |
8 | if (heightmap) {
9 | this
10 | .modifyEachVertex((vertex, index) => {
11 | vertex.y = heightmap[index];
12 | })
13 | .computeNormals()
14 | .done_();
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/engine/renderer/camera.ts:
--------------------------------------------------------------------------------
1 | import { Object3d } from './object-3d';
2 |
3 | export class Camera extends Object3d {
4 | projection: DOMMatrix;
5 |
6 | constructor(fieldOfViewRadians: number, aspect: number, near: number, far: number) {
7 | super();
8 |
9 | const f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewRadians);
10 | const rangeInv = 1.0 / (near - far);
11 |
12 | this.projection = new DOMMatrix([
13 | f / aspect, 0, 0, 0,
14 | 0, f, 0, 0,
15 | 0, 0, (near + far) * rangeInv, -1,
16 | 0, 0, near * far * rangeInv * 2, 0
17 | ]);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/core/state-machine.ts:
--------------------------------------------------------------------------------
1 | import { State } from './state';
2 |
3 | export class StateMachine {
4 | private currentState: State;
5 |
6 | constructor(initialState: State, ...enterArgs: any) {
7 | this.currentState = initialState;
8 | this.currentState.onEnter?.(...enterArgs);
9 | }
10 |
11 | async setState(newState: State, ...enterArgs: any) {
12 | await this.currentState.onLeave?.();
13 | this.currentState = { onUpdate: () => {} };
14 | await newState.onEnter?.(...enterArgs);
15 | this.currentState = newState;
16 | }
17 |
18 | getState() {
19 | return this.currentState;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/engine/physics/face.ts:
--------------------------------------------------------------------------------
1 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point";
2 | import { calculateFaceNormal } from '@/engine/helpers';
3 |
4 | export class Face {
5 | points: EnhancedDOMPoint[];
6 | normal: EnhancedDOMPoint;
7 | upperY: number;
8 | lowerY: number;
9 | originOffset: number;
10 |
11 | constructor(points: EnhancedDOMPoint[], normal?: EnhancedDOMPoint) {
12 | this.points = points;
13 | this.normal = normal ?? calculateFaceNormal(points);
14 | this.originOffset = -this.normal.dot(points[0]);
15 | const ys = points.map(point => point.y);
16 | this.upperY = Math.max(...ys);
17 | this.lowerY = Math.min(...ys);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/engine/shaders/vertex.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | layout(location = 0) in vec3 aCoords;
4 | layout(location = 1) in vec3 aNormal;
5 | layout(location = 2) in vec2 aTexCoord;
6 | layout(location = 3) in float aDepth;
7 |
8 | uniform mat4 modelviewProjection;
9 | uniform mat4 normalMatrix;
10 | uniform mat4 lightPovMvp;
11 |
12 | out vec2 vTexCoord;
13 | out float vDepth;
14 | out vec3 vNormal;
15 | out mat4 vNormalMatrix;
16 | out vec4 positionFromLightPov;
17 |
18 | void main() {
19 | vec4 coords = vec4(aCoords, 1.0);
20 | gl_Position = modelviewProjection * coords;
21 |
22 | vTexCoord = aTexCoord;
23 | vDepth = aDepth;
24 | vNormal = aNormal;
25 | vNormalMatrix = normalMatrix;
26 | positionFromLightPov = lightPovMvp * coords;
27 | }
28 |
--------------------------------------------------------------------------------
/src/draw-helpers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LengthOrPercentage,
3 | rect,
4 | svg,
5 | SvgAttributes,
6 | SvgString,
7 | SvgTextAttributes,
8 | } from '@/engine/svg-maker/base';
9 | import { drawBloodText } from '@/textures';
10 |
11 | export function overlaySvg(additionalAttributes?: Partial, ...elements: string[]): SvgString {
12 | return svg({...additionalAttributes, viewBox: `0 0 1920 1080` }, ...elements);
13 | }
14 |
15 | export function drawFullScreenText(text: string, fontSize = 250) {
16 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' },
17 | rect({x: 0, y: 0, width_: '100%', height_: '100%' }),
18 | drawBloodText({ x: '50%', y: '52%', style: `font-size: ${fontSize}px; text-shadow: 1px 1px 20px` }, text, 40),
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/engine/renderer/texture-loader.ts:
--------------------------------------------------------------------------------
1 | import { gl } from '@/engine/renderer/lil-gl';
2 | import { Texture } from '@/engine/renderer/texture';
3 |
4 | class TextureLoader {
5 | textures: Texture[] = [];
6 |
7 | load_(textureSource: TexImageSource): Texture {
8 | const texture = new Texture(this.textures.length, textureSource);
9 | this.textures.push(texture);
10 | return texture;
11 | }
12 |
13 | bindTextures() {
14 | gl.activeTexture(gl.TEXTURE0);
15 | gl.bindTexture(gl.TEXTURE_2D_ARRAY, gl.createTexture());
16 | gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 8, gl.RGBA8, 512, 512, this.textures.length);
17 |
18 | this.textures.forEach((texture, index) => {
19 | gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, index, 512, 512, 1, gl.RGBA, gl.UNSIGNED_BYTE, texture.source);
20 | });
21 | gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
22 | }
23 | }
24 |
25 | export const textureLoader = new TextureLoader();
26 |
--------------------------------------------------------------------------------
/src/engine/skybox.ts:
--------------------------------------------------------------------------------
1 | import { AttributeLocation } from '@/engine/renderer/renderer';
2 | import { gl } from '@/engine/renderer/lil-gl';
3 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry';
4 |
5 | export class Skybox extends MoldableCubeGeometry {
6 | constructor(...textureSources: TexImageSource[]) {
7 | super();
8 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
9 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, gl.createTexture());
10 | textureSources.forEach((tex, index) => {
11 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + index, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, tex);
12 | });
13 | gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
14 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
15 | this.setAttribute_(AttributeLocation.Positions, new Float32Array([
16 | -1, -1,
17 | 1, -1,
18 | -1, 1,
19 | -1, 1,
20 | 1, -1,
21 | 1, 1,
22 | ]), 2);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/engine/new-new-noise.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ellipse,
3 | feTurbulence,
4 | filter,
5 | NoiseType, radialGradient,
6 | rect,
7 | svg, svgStop,
8 | } from '@/engine/svg-maker/base';
9 | import { toHeightmap } from '@/engine/svg-maker/converters';
10 |
11 |
12 | export async function newNoiseLandscape(size: number,seed_: number, baseFrequency: number, numOctaves_: number, type_: NoiseType, scale_: number) {
13 | const s = svg({ width_: 256, height_: 256 },
14 | filter({ id_: 'n' },
15 | feTurbulence({ seed_, baseFrequency, numOctaves_, type_ }),
16 | ),
17 | radialGradient({ id_: 'l' },
18 | svgStop({ offset_: '10%', stopColor: '#0004' }),
19 | svgStop({ offset_: '22%', stopColor: '#0000' }),
20 | ),
21 | rect({ x: 0, y: 0, width_: '100%', height_: '100%', filter: 'n' }),
22 | ellipse({ cx: 128, cy: 128, fill: 'url(#l)', rx: 200, ry: 200 }),
23 | //
24 | ellipse({ cx: 128, cy: 128, fill: '#afafaf', rx: 26, ry: 26 }),
25 |
26 | // rect({ x: 109, y: 109, width_: 38, height_: 42, fill: '#afafaf' }),
27 | rect({ x: 125, y: 10, width_: 6, height_: 80, fill: '#afafaf' })
28 | );
29 | return toHeightmap(s, scale_);
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript-template",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "devDependencies": {
6 | "@rollup/plugin-typescript": "^8.5.0",
7 | "@typescript-eslint/eslint-plugin": "^5.52.0",
8 | "@typescript-eslint/parser": "^5.52.0",
9 | "clean-css": "^5.3.1",
10 | "copy-webpack-plugin": "^9.0.1",
11 | "cross-env": "^7.0.3",
12 | "ect-bin": "1.4.1",
13 | "eslint": "^8.34.0",
14 | "eslint-plugin-unused-imports": "^2.0.0",
15 | "google-closure-compiler": "^20230206.0.0",
16 | "html-minifier": "^4.0.0",
17 | "html-webpack-plugin": "^5.3.2",
18 | "npm-run-all": "^4.1.5",
19 | "roadroller": "^2.1.0",
20 | "tmp": "^0.2.1",
21 | "ts-loader": "^9.2.5",
22 | "typescript": "^4.9.5",
23 | "vite": "^4.1.3",
24 | "webpack": "^5.50.0",
25 | "webpack-cli": "^4.8.0",
26 | "webpack-dev-server": "^4.0.0"
27 | },
28 | "scripts": {
29 | "serve": "npm-run-all --parallel tsc vite",
30 | "build": "cross-env LEVEL_2_BUILD=true vite build",
31 | "find-best-roadroller": "node find-best-roadroller.js",
32 | "build-with-best-roadroller": "cross-env USE_RR_CONFIG=true vite build",
33 | "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore src",
34 | "vite": "vite serve",
35 | "tsc": "tsc --watch --noEmit"
36 | },
37 | "author": "Rob Louie"
38 | }
39 |
--------------------------------------------------------------------------------
/src/engine/renderer/scene.ts:
--------------------------------------------------------------------------------
1 | import { Object3d } from '@/engine/renderer/object-3d';
2 | import { Skybox } from '@/engine/skybox';
3 | import { Mesh } from '@/engine/renderer/mesh';
4 |
5 | export class Scene extends Object3d {
6 | skybox?: Skybox;
7 | solidMeshes: Mesh[] = [];
8 | transparentMeshes: Mesh[] = [];
9 |
10 | add_(...object3ds: Object3d[]) {
11 | super.add_(...object3ds);
12 | const flatWithChildren = [...object3ds, ...object3ds.flatMap(object3d => object3d.allChildren())];
13 | flatWithChildren.forEach(object3d => {
14 | // @ts-ignore
15 | if (object3d.geometry) {
16 | // @ts-ignore
17 | object3d.geometry.bindGeometry();
18 | // @ts-ignore
19 | object3d.material.isTransparent ? this.transparentMeshes.push(object3d) : this.solidMeshes.push(object3d);
20 | }
21 | })
22 | }
23 |
24 | remove_(object3d: Object3d) {
25 | super.remove_(object3d);
26 | [object3d, ...object3d.allChildren()]
27 | .forEach(obj => {
28 | // @ts-ignore
29 | if (obj.geometry) {
30 | // @ts-ignore
31 | if (obj.material.isTransparent) {
32 | this.transparentMeshes = this.transparentMeshes.filter(mesh => mesh !== obj);
33 | } else {
34 | this.solidMeshes = this.solidMeshes.filter(mesh => mesh !== obj);
35 | }
36 | }
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createGameStateMachine, gameStateMachine } from './game-state-machine';
2 | import { controls } from '@/core/controls';
3 | import { initTextures } from '@/textures';
4 | import { GameState } from '@/game-states/game.state';
5 | import { gameStates } from '@/game-states/game-states';
6 | import { MenuState } from '@/game-states/menu.state';
7 | import { drawFullScreenText } from '@/draw-helpers';
8 | import { castleContainer, createCastle } from '@/modeling/castle';
9 |
10 | let previousTime = 0;
11 | const interval = 1000 / 60;
12 |
13 | (() => {
14 | drawFullScreenText('CLICK TO START', 200);
15 | document.onclick = async () => {
16 | drawFullScreenText('LOADING');
17 |
18 | await initTextures();
19 | castleContainer.value = createCastle().translate_(0, 21).done_();
20 |
21 | gameStates.gameState = new GameState();
22 | gameStates.menuState = new MenuState();
23 |
24 | createGameStateMachine(gameStates.menuState);
25 |
26 | draw(0);
27 |
28 | document.onclick = null;
29 | };
30 |
31 | function draw(currentTime: number) {
32 | const delta = currentTime - previousTime;
33 |
34 | if (delta >= interval) {
35 | previousTime = currentTime - (delta % interval);
36 |
37 | controls.queryController();
38 | gameStateMachine.getState().onUpdate(delta);
39 | }
40 | requestAnimationFrame(draw);
41 | }
42 | })();
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/engine/svg-maker/converters.ts:
--------------------------------------------------------------------------------
1 | import { SvgString } from '@/engine/svg-maker/base';
2 |
3 | export function toImageDom(svgString: SvgString) {
4 | const image_ = document.createElement('img');
5 | image_.src = `type:image/svg+xml,${btoa(svgString)}`;
6 | return image_;
7 | }
8 |
9 | export function toObjectUrl(svgString: SvgString) {
10 | return URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml' }));
11 | }
12 |
13 | export async function toImage(svgImageBuilder: SvgString): Promise {
14 | const image_ = new Image();
15 | image_.src = toObjectUrl(svgImageBuilder);
16 | return new Promise(resolve => image_.addEventListener('load', () => resolve(image_)));
17 | }
18 |
19 | export async function toImageData(svgString: SvgString): Promise {
20 | const image_ = await toImage(svgString);
21 | const canvas = new OffscreenCanvas(image_.width, image_.height);
22 | const context = canvas.getContext('2d')!;
23 | // @ts-ignore
24 | context.drawImage(image_, 0, 0);
25 | // @ts-ignore
26 | return context.getImageData(0, 0, image_.width, image_.height);
27 | }
28 |
29 | export async function toHeightmap(svgString: SvgString, scale_: number): Promise {
30 | const imageData = await toImageData(svgString);
31 | return [...imageData.data]
32 | .filter((value, index) => !(index % 4))
33 | .map(value => {
34 | return (value / 255 - 0.5) * scale_;
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/engine/helpers.ts:
--------------------------------------------------------------------------------
1 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
2 |
3 |
4 | export function doTimes(times: number, callback: (index: number) => T): T[] {
5 | const result: T[] = [];
6 | for (let i = 0; i < times; i++) {
7 | result.push(callback(i));
8 | }
9 | return result;
10 | }
11 |
12 | export function clamp(value: number, min: number, max: number): number {
13 | return Math.min(Math.max(value, min), max);
14 | }
15 |
16 |
17 | export function radsToDegrees(radians: number): number {
18 | return radians * (180 / Math.PI);
19 | }
20 |
21 | function unormalizedNormal(points: EnhancedDOMPoint[]): EnhancedDOMPoint {
22 | const u = points[2].clone_().subtract(points[1]);
23 | const v = points[0].clone_().subtract(points[1]);
24 | return new EnhancedDOMPoint().crossVectors(u, v);
25 | }
26 |
27 | export function calculateFaceNormal(points: EnhancedDOMPoint[]): EnhancedDOMPoint {
28 | return unormalizedNormal(points).normalize_();
29 | }
30 |
31 | export function calculateVertexNormals(points: EnhancedDOMPoint[], indices: number[] | Uint16Array): EnhancedDOMPoint[] {
32 | const vertexNormals = points.map(_ => new EnhancedDOMPoint());
33 | for (let i = 0; i < indices.length; i+= 3) {
34 | const faceNormal = unormalizedNormal([points[indices[i]], points[indices[i + 1]], points[indices[i + 2]]]);
35 | vertexNormals[indices[i]].add_(faceNormal);
36 | vertexNormals[indices[i + 1]].add_(faceNormal);
37 | vertexNormals[indices[i + 2]].add_(faceNormal);
38 | }
39 |
40 | return vertexNormals.map(vector => vector.normalize_());
41 | }
42 |
--------------------------------------------------------------------------------
/src/engine/shaders/fragment.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | //[
4 | precision highp float;
5 | //]
6 | in vec4 vColor;
7 | in vec2 vTexCoord;
8 | in float vDepth;
9 | in vec3 vNormal;
10 | in mat4 vNormalMatrix;
11 | in vec4 positionFromLightPov;
12 |
13 | uniform vec2 textureRepeat;
14 | uniform vec4 emissive;
15 | uniform mediump sampler2DArray uSampler;
16 | uniform mediump sampler2DShadow shadowMap;
17 |
18 | out vec4 outColor;
19 |
20 | vec3 light_direction = vec3(-1, 1.5, -1);
21 |
22 | float ambientLight = 0.2f;
23 | float maxLit = 0.6f;
24 |
25 | vec2 adjacentPixels[5] = vec2[](
26 | vec2(0, 0),
27 | vec2(-1, 0),
28 | vec2(1, 0),
29 | vec2(0, 1),
30 | vec2(0, -1)
31 | );
32 |
33 | float visibility = 1.0;
34 | float shadowSpread = 4200.0;
35 |
36 | void main() {
37 | for (int i = 0; i < 5; i++) {
38 | vec3 samplePosition = vec3(positionFromLightPov.xy + adjacentPixels[i]/shadowSpread, positionFromLightPov.z - 0.001);
39 | float hitByLight = texture(shadowMap, samplePosition);
40 | visibility *= max(hitByLight, 0.87);
41 | }
42 |
43 | vec3 correctedNormals = normalize(mat3(vNormalMatrix) * vNormal);
44 | vec3 normalizedLightPosition = normalize(light_direction);
45 | float litPercent = max(dot(normalizedLightPosition, correctedNormals) * visibility, ambientLight);
46 |
47 |
48 | vec3 litColor = length(emissive) > 0.0 ? emissive.rgb : (litPercent * vec3(1.0, 1.0, 1.0));
49 |
50 | vec4 vColor = vec4(litColor.rgb, 1.0);
51 |
52 | if (vDepth < 0.0) {
53 | outColor = vColor;
54 | } else {
55 | outColor = texture(uSampler, vec3(vTexCoord * textureRepeat, vDepth)) * vColor;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/core/controls.ts:
--------------------------------------------------------------------------------
1 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
2 |
3 | class Controls {
4 | isConfirm? = false;
5 | inputDirection: EnhancedDOMPoint;
6 | private mouseMovement = new EnhancedDOMPoint();
7 | private onMouseMoveCallback?: (mouseMovement: EnhancedDOMPoint) => void;
8 |
9 | keyMap: Map = new Map();
10 |
11 | constructor() {
12 | document.addEventListener('keydown', event => this.toggleKey(event, true));
13 | document.addEventListener('keyup', event => this.toggleKey(event, false));
14 | document.addEventListener('mousedown', () => this.toggleKey({ code: 'KeyE' }, true));
15 | document.addEventListener('mouseup', () => this.toggleKey({ code: 'KeyE' }, false));
16 |
17 | document.addEventListener('mousemove', event => {
18 | this.mouseMovement.x = event.movementX;
19 | this.mouseMovement.y = event.movementY;
20 | this.onMouseMoveCallback?.(this.mouseMovement);
21 | });
22 | this.inputDirection = new EnhancedDOMPoint();
23 | }
24 |
25 | onMouseMove(callback: (mouseMovement: EnhancedDOMPoint) => void) {
26 | this.onMouseMoveCallback = callback;
27 | }
28 |
29 | queryController() {
30 | const leftVal = (this.keyMap.get('KeyA') || this.keyMap.get('ArrowLeft')) ? -1 : 0;
31 | const rightVal = (this.keyMap.get('KeyD') || this.keyMap.get('ArrowRight')) ? 1 : 0;
32 | const upVal = (this.keyMap.get('KeyW') || this.keyMap.get('ArrowUp')) ? -1 : 0;
33 | const downVal = (this.keyMap.get('KeyS') || this.keyMap.get('ArrowDown')) ? 1 : 0;
34 | this.inputDirection.x = (leftVal + rightVal);
35 | this.inputDirection.y = (upVal + downVal);
36 | this.isConfirm = this.keyMap.get('KeyE');
37 | }
38 |
39 | private toggleKey(event: { code: string }, isPressed: boolean) {
40 | this.keyMap.set(event.code, isPressed);
41 | }
42 | }
43 |
44 | export const controls = new Controls();
45 |
--------------------------------------------------------------------------------
/src/engine/physics/parse-faces.ts:
--------------------------------------------------------------------------------
1 | import { Face } from './face';
2 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point";
3 | import { AttributeLocation } from '@/engine/renderer/renderer';
4 | import { Mesh } from '@/engine/renderer/mesh';
5 |
6 | function indexToFaceVertexPoint(index: number, positionData: Float32Array, matrix: DOMMatrix): EnhancedDOMPoint {
7 | return new EnhancedDOMPoint().set(
8 | matrix.transformPoint(new EnhancedDOMPoint(positionData[index], positionData[index + 1], positionData[index + 2]))
9 | )
10 | }
11 |
12 | export function meshToFaces(meshes: Mesh[], transformMatrix?: DOMMatrix) {
13 | return meshes.flatMap(mesh => {
14 | const indices = mesh.geometry.getIndices();
15 |
16 | const positions = mesh.geometry.getAttribute_(AttributeLocation.Positions);
17 | const triangles = [];
18 | for (let i = 0; i < indices.length; i += 3) {
19 | const firstIndex = indices[i] * 3;
20 | const secondIndex = indices[i + 1] * 3;
21 | const thirdIndex = indices[i + 2] * 3;
22 |
23 | const point0 = indexToFaceVertexPoint(firstIndex, positions.data, transformMatrix ?? mesh.worldMatrix);
24 | const point1 = indexToFaceVertexPoint(secondIndex, positions.data, transformMatrix ?? mesh.worldMatrix);
25 | const point2 = indexToFaceVertexPoint(thirdIndex, positions.data, transformMatrix ?? mesh.worldMatrix);
26 |
27 | triangles.push([
28 | point0,
29 | point1,
30 | point2,
31 | ]);
32 | }
33 |
34 | return triangles.map(triangle => new Face(triangle));
35 | });
36 | }
37 |
38 | export function getGroupedFaces(faces: Face[]) {
39 | const result: {floorFaces: Face[], wallFaces: Face[]} = { floorFaces: [], wallFaces: [] };
40 | faces.forEach(face => {
41 | if (face.normal.y > 0.2) {
42 | result.floorFaces.push(face);
43 | } else {
44 | result.wallFaces.push(face);
45 | }
46 | });
47 | return result;
48 | }
49 |
--------------------------------------------------------------------------------
/src/sound-effects.ts:
--------------------------------------------------------------------------------
1 | import { addGap, createAudioNode, createPannerNode, zzfxG } from '@/engine/audio/audio-player';
2 |
3 | const pannerFunc = navigator.userAgent.includes('fox') ? createAudioNode : createPannerNode;
4 |
5 | export const outsideFootsteps = createAudioNode(addGap(zzfxG(...[.1,.65,986,.01,.03,.03,4,2.63,,,,,.25,6.8,,,.09,.14,.01]), 0.3));
6 | export const indoorFootsteps = createAudioNode(zzfxG(...[.1,.65,1100,,.03,.42,4,2.63,,,,,,2.2,,,,.14,.01]));
7 |
8 | export const doorCreak = pannerFunc(zzfxG(...[.1,,349,.57,.51,1,2,,2.4,-2,,.09,,.4,,,.02,1.3,.72]));
9 |
10 | // export const draggingSound = createPannerNode(zzfxG(...[1.09,,152,.11,1,1,1,2.19,4,,-10,,.06,1.1,51,.2,.16,.86,.5,.31]));
11 | export const draggingSound2 = pannerFunc(zzfxG(...[0.05,0,50,.1,1.3,,4,9,,,-10,.01,,,,-0.2,,.87,1]));
12 |
13 | // export const draggingSound3 = createPannerNode(zzfxG(...[,.15,50,.34,1,1,,.11,-1.9,-0.9,20,3,.18,5,,,.31,1.01,1,.08]));
14 |
15 | export const draggingSound4 = pannerFunc(zzfxG(...[0.2,.15,50,.34,.91,.31,,.11,-1.9,-0.9,20,3,.01,7.2,,,.15,1.01,1]));
16 |
17 | export const ominousDiscovery1 = createAudioNode(zzfxG(...[,0,146.8324,.3,.3,.9,,.83,10.5,,40,.3,-0.01,,,.1,.2,,.3]));
18 |
19 | export const ominousDiscovery2 = createAudioNode(zzfxG(...[3,,87.30706,.08,.41,1.41,1,.83,10.5,,3,.5,-0.01,.1,,.1,.78,,.11,.14]));
20 |
21 | export const pickup1 = createAudioNode(zzfxG(...[1.09,,152,.01,.08,1,,2.19,,,,,.05,1.1,51,.2,.02,.86,.04]));
22 |
23 | export const scaryNote2 = (vol = 1) => createAudioNode(addGap(zzfxG(...[vol,0,50,.6,.1,1,,.11,-0.1,-0.1,,,.18,,,,.3,1.01,.5,.08]), .8));
24 |
25 | export const upyriAttack = createAudioNode(zzfxG(...[3.2,0,276,,2,,,.71,7.5,,,,,.1,-430,.5,.19,.2,.2]));
26 | export const upyriAttack2 = createAudioNode(zzfxG(...[2.4,0,50,.01,.1,1,,.11,-0.1,-0.1,,,.18,,,,.31,.3,.5,.08]));
27 |
28 | export const upyriHit = createPannerNode(zzfxG(...[1.65,,57,.01,.09,.08,4,.11,,,,,.15,1.3,,.2,.08,.72,.04]));
29 |
30 | const music = (freqs: number[], durs: number[]) => createAudioNode(freqs.flatMap((freq, i) => zzfxG(...[,0,freq,.1,durs[i],.26,2,1.18,,,,,,.2,,,.11,.38,.03])));
31 |
32 | export const makeSong = music([65.41 , 82.41, 77.78, 65.41 , 82.41, 77.78, 65.41 , 82.41, 77.78, 61.74, 65.41], [.25, .25, 1, .25, .25, 1, .25, .25, .25, .25, 2])
33 |
--------------------------------------------------------------------------------
/src/engine/audio/audio-player.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
3 |
4 | export const audioCtx = new AudioContext();
5 |
6 | // zzfxV - global volume
7 | const zzfxV=.7;
8 |
9 | // zzfxR - global sample rate
10 | const zzfxR=44100
11 |
12 | // zzfxP() - the sound player -- returns a AudioBufferSourceNode
13 | export const zzfxP=(...t)=>{let e=audioCtx.createBufferSource(),f=audioCtx.createBuffer(t.length,t[0].length,zzfxR);t.map((d,i)=>f.getChannelData(i).set(d)),e.buffer=f;return e}
14 |
15 | // zzfxG() - the sound generator -- returns an array of sample data
16 | export const zzfxG = (p=1,k=.05,b=220,e=0,r=0,t=.1,q=0,D=1,u=0,y=0,v=0,z=0,l=0,E=0,A=0,F=0,c=0,w=1,m=0,B=0): number[] =>{let d=2*Math.PI,G=u*=500*d/zzfxR/zzfxR,C=b*=(1-k+2*k*Math.random(k=[]))*d/zzfxR,g=0,H=0,a=0,n=1,I=0,J=0,f=0,x,h;e=zzfxR*e+9;m*=zzfxR;r*=zzfxR;t*=zzfxR;c*=zzfxR;y*=500*d/zzfxR**3;A*=d/zzfxR;v*=d/zzfxR;z*=zzfxR;l=zzfxR*l|0;for(h=e+m+ r+t+c|0;aa?0:(az&&(b+=v,C+=v,n=0),!l||++I%l||(b=C,u=G,n=n||1);return k};
17 |
18 | export function createPannerNode(buffer: number[]) {
19 | return (position_: EnhancedDOMPoint) => {
20 | const panner = new PannerNode(audioCtx, {
21 | distanceModel: 'linear',
22 | positionX: position_.x,
23 | positionY: position_.y,
24 | positionZ: position_.z,
25 | maxDistance: 60,
26 | rolloffFactor: 40,
27 | coneOuterGain: 0.4
28 | });
29 | const node = zzfxP(buffer);
30 | node.connect(panner).connect(audioCtx.destination);
31 | return node;
32 | }
33 | }
34 |
35 | export function createAudioNode(buffer: number[]) {
36 | return () => {
37 | const node = zzfxP(buffer)
38 | node.connect(audioCtx.destination);
39 | return node;
40 | }
41 | }
42 |
43 |
44 | export function addGap(buffer: number[], seconds: number) {
45 | for (let i = 0; i < seconds * zzfxR; i++) {
46 | buffer.push(0);
47 | }
48 | return buffer;
49 | }
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/engine/renderer/lil-gl.ts:
--------------------------------------------------------------------------------
1 | import {
2 | depth_fragment_glsl,
3 | depth_vertex_glsl,
4 | fragment_glsl, shadowMap, skybox_fragment_glsl, skybox_vertex_glsl, uSampler, vertex_glsl
5 | } from '@/engine/shaders/shaders';
6 |
7 | export class LilGl {
8 | gl: WebGL2RenderingContext;
9 | program: WebGLProgram;
10 | skyboxProgram: WebGLProgram;
11 | depthProgram: WebGLProgram;
12 |
13 | constructor() {
14 | // @ts-ignore
15 | this.gl = c3d.getContext('webgl2')!;
16 | const vertex = this.createShader(this.gl.VERTEX_SHADER, vertex_glsl);
17 | const fragment = this.createShader(this.gl.FRAGMENT_SHADER, fragment_glsl);
18 | this.program = this.createProgram(vertex, fragment);
19 | const skyboxVertex = this.createShader(this.gl.VERTEX_SHADER, skybox_vertex_glsl);
20 | const skyboxFragment = this.createShader(this.gl.FRAGMENT_SHADER, skybox_fragment_glsl);
21 | this.skyboxProgram = this.createProgram(skyboxVertex, skyboxFragment);
22 | const depthVertex = this.createShader(this.gl.VERTEX_SHADER, depth_vertex_glsl);
23 | const depthFragment = this.createShader(this.gl.FRAGMENT_SHADER, depth_fragment_glsl);
24 | this.depthProgram = this.createProgram(depthVertex, depthFragment);
25 |
26 | const shadowMapLocation = this.gl.getUniformLocation(this.program, shadowMap);
27 | const textureLocation = this.gl.getUniformLocation(this.program, uSampler);
28 | this.gl.useProgram(this.program);
29 | this.gl.uniform1i(textureLocation, 0);
30 | this.gl.uniform1i(shadowMapLocation, 1);
31 |
32 | }
33 |
34 | createShader(type: GLenum, source: string): WebGLShader {
35 | const shader = this.gl.createShader(type)!;
36 | this.gl.shaderSource(shader, source);
37 | this.gl.compileShader(shader);
38 | return shader;
39 | }
40 |
41 | createProgram(vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram {
42 | const program = this.gl.createProgram()!;
43 | this.gl.attachShader(program, vertexShader);
44 | this.gl.attachShader(program, fragmentShader);
45 | this.gl.linkProgram(program);
46 |
47 | if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
48 | console.log(this.gl.getShaderInfoLog(vertexShader));
49 | console.log(this.gl.getShaderInfoLog(fragmentShader));
50 | }
51 |
52 | return program;
53 | }
54 | }
55 |
56 | export const lilgl = new LilGl();
57 | export const gl = lilgl.gl;
58 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | parser: '@typescript-eslint/parser',
7 | parserOptions: {
8 | ecmaVersion: 2020,
9 | sourceType: "module"
10 | },
11 | plugins: ['unused-imports', '@typescript-eslint'],
12 | rules: {
13 | 'no-console': 'off',
14 | 'no-debugger': 'off',
15 | 'prefer-destructuring': 'off',
16 | camelcase: 'off',
17 | 'no-use-before-define': ['error', { variables: true, functions: false, classes: true }],
18 | 'max-classes-per-file': ['error', 1],
19 | 'no-global-assign': ['error', { exceptions: ['Object'] }],
20 | 'no-unneeded-ternary': 'error',
21 | 'import/prefer-default-export': 'off',
22 | 'guard-for-in': 'error',
23 | 'arrow-parens': 'off',
24 | semi: 'warn',
25 | 'arrow-body-style': 'off',
26 | 'no-multiple-empty-lines': ['error', { max: 2, maxBOF: 1 }],
27 | 'lines-between-class-members': 'off',
28 | yoda: 'error',
29 | 'no-unused-vars': 'off',
30 | 'no-bitwise': 'off',
31 | 'no-plusplus': 'off',
32 | 'class-methods-use-this': 'warn',
33 | 'linebreak-style': 0,
34 | 'function-paren-newline': 'off',
35 | 'unused-imports/no-unused-imports': 'warn',
36 | 'id-denylist': [
37 | 'warn', 'seed', 'direction', 'clone', 'normalize', 'setAttribute', 'done', 'all', 'translate', 'scale', 'rotate',
38 | 'position', 'rotation', 'children', 'parent', 'remove', 'setRotation', 'textureRepeat', 'load', 'image',
39 | 'width', 'height', 'offset', 'style', 'color', 'gradientTransform', 'operator', 'radius', 'result', 'stitchTiles',
40 | 'surfaceScale', 'baseFrequency', 'viewBox'
41 | ],
42 | 'unused-imports/no-unused-vars': [
43 | 'warn',
44 | {
45 | vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_',
46 | },
47 | ],
48 | '@typescript-eslint/no-explicit-any': 'off',
49 | '@typescript-eslint/explicit-module-boundary-types': 'off',
50 | 'no-shadow': 'off',
51 | '@typescript-eslint/no-shadow': ['error'],
52 | },
53 | overrides: [
54 | {
55 | files: [
56 | '**/__tests__/*.{j,t}s?(x)',
57 | '**/tests/unit/**/*.spec.{j,t}s?(x)',
58 | ],
59 | env: {
60 | jest: true,
61 | },
62 | },
63 | ],
64 | settings: {
65 | 'import/resolver': {
66 | typescript: {},
67 | },
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/find-best-roadroller.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('node:child_process');
2 | const readline = require('readline');
3 | const fs = require('fs');
4 |
5 | const rl = readline.createInterface({
6 | input: process.stdin,
7 | output: process.stdout
8 | });
9 |
10 | const cliToApiMaps = [
11 | { cli: '-Zab', api: 'numAbbreviations', type: 'number' },
12 | { cli: '-Zlr', api: 'recipLearningRate', type: 'number' },
13 | { cli: '-Zmc', api: 'modelMaxCount', type: 'number' },
14 | { cli: '-Zmd', api: 'modelRecipBaseCount', type: 'number' },
15 | { cli: '-Zpr', api: 'precision', type: 'number' },
16 | { cli: '-Zdy', api: 'dynamicModels', type: 'number' },
17 | { cli: '-Zco', api: 'contextBits', type: 'number' },
18 | { cli: '-S', api: 'sparseSelectors', type: 'array' }
19 | ]
20 |
21 | rl.question('How many seconds should RoadRoller spend looking for the best config? ', seconds => {
22 | console.log('Building...');
23 | exec('vite build', () => {
24 | console.log(`Spending ${seconds} seconds searching for config...`);
25 | exec(`node node_modules/roadroller/cli.mjs ${__dirname}/dist/output.js -D -OO`, { timeout: seconds * 1000, killSignal: 'SIGINT', maxBuffer: 4069 * 1024 }, (error, stdout, stderr) => {
26 | const bestConfigJs = { allowFreeVars: true };
27 | const bestConfigConsole = stderr.split('\n').reverse().find(line => line.includes('<-'));
28 | const itemCheckRemoved = bestConfigConsole.split(') ')[1];
29 | const endSizeRemoved = itemCheckRemoved.split(': ')[0];
30 | const configPieces = endSizeRemoved.split(' ').filter(param => !param.startsWith('-Sx'));
31 | configPieces.forEach(singleParam => {
32 | cliToApiMaps.forEach(mapper => {
33 | if (singleParam.startsWith(mapper.cli) && mapper.type !== 'unused') {
34 | bestConfigJs[mapper.api] = convertValue(mapper, singleParam);
35 | }
36 | })
37 | });
38 | fs.writeFileSync(`${__dirname}/roadroller-config.json`, JSON.stringify(bestConfigJs, null, 2));
39 | console.log(`BEST CONFIG: ${bestConfigConsole}`);
40 | process.exit(0);
41 | });
42 | })
43 | });
44 |
45 | function convertValue(mapper, cliSetting) {
46 | const stringValue = cliSetting.replace(mapper.cli, '');
47 | if (mapper.type === 'number') {
48 | return parseInt(stringValue, 10);
49 | } else {
50 | return stringValue.split(',').map(value => parseInt(value, 10));
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/engine/shaders/shaders.ts:
--------------------------------------------------------------------------------
1 | // Generated with Shader Minifier 1.3.4 (https://github.com/laurentlb/Shader_Minifier/)
2 | export const aCoords = 'E';
3 | export const aDepth = 'L';
4 | export const aNormal = 'B';
5 | export const aPosition = 'm';
6 | export const aTexCoord = 'K';
7 | export const emissive = 't';
8 | export const fragDepth = 'v';
9 | export const lightPovMvp = 'f';
10 | export const modelviewProjection = 'M';
11 | export const normalMatrix = 'N';
12 | export const outColor = 'g';
13 | export const positionFromLightPov = 'u';
14 | export const shadowMap = 'z';
15 | export const textureRepeat = 'h';
16 | export const uSampler = 's';
17 | export const u_skybox = 'I';
18 | export const u_viewDirectionProjectionInverse = 'H';
19 | export const vColor = 'e';
20 | export const vDepth = 'n';
21 | export const vNormal = 'o';
22 | export const vNormalMatrix = 'l';
23 | export const vTexCoord = 'i';
24 | export const v_position = 'G';
25 |
26 | export const depth_fragment_glsl = `#version 300 es
27 | precision highp float;
28 | out float v;void main(){v=gl_FragCoord.z;}`;
29 |
30 | export const depth_vertex_glsl = `#version 300 es
31 | precision highp float;
32 | layout(location=0) in vec4 m;uniform mat4 f;void main(){gl_Position=f*m;}`;
33 |
34 | export const fragment_glsl = `#version 300 es
35 | precision highp float;
36 | in vec4 e;in vec2 i;in float n;in vec3 o;in mat4 l;in vec4 u;uniform vec2 h;uniform vec4 t;uniform mediump sampler2DArray s;uniform mediump sampler2DShadow z;out vec4 g;vec3 d=vec3(-1,1.5,-1);float A=.2f,C=.6f;vec2 D[5]=vec2[](vec2(0),vec2(-1,0),vec2(1,0),vec2(0,1),vec2(0,-1));float F=1.,J=4200.;void main(){for(int v=0;v<5;v++){vec3 m=vec3(u.xy+D[v]/J,u.z-.001);float f=texture(z,m);F*=max(f,.87);}vec3 v=normalize(mat3(l)*o),f=normalize(d);float m=max(dot(f,v)*F,A);vec3 e=length(t)>0.?t.xyz:m*vec3(1);vec4 C=vec4(e.xyz,1);g=n<0.?C:texture(s,vec3(i*h,n))*C;}`;
37 |
38 | export const skybox_fragment_glsl = `#version 300 es
39 | precision highp float;
40 | uniform samplerCube I;uniform mat4 H;in vec4 G;out vec4 g;void main(){vec4 v=H*G;g=texture(I,v.xyz);}`;
41 |
42 | export const skybox_vertex_glsl = `#version 300 es
43 | layout(location=0) in vec4 E;out vec4 G;void main(){G=E;gl_Position=E;gl_Position.z=1.;}`;
44 |
45 | export const vertex_glsl = `#version 300 es
46 | layout(location=0) in vec3 E;layout(location=1) in vec3 B;layout(location=2) in vec2 K;layout(location=3) in float L;uniform mat4 M,N,f;out vec2 i;out float n;out vec3 o;out mat4 l;out vec4 u;void main(){vec4 v=vec4(E,1);gl_Position=M*v;i=K;n=L;o=B;l=N;u=f*v;}`;
47 |
48 |
--------------------------------------------------------------------------------
/src/engine/enhanced-dom-point.ts:
--------------------------------------------------------------------------------
1 | export interface VectorLike {
2 | x: number;
3 | y: number;
4 | z: number;
5 | w?: number;
6 | }
7 |
8 | export class EnhancedDOMPoint extends DOMPoint {
9 | add_(otherVector: VectorLike) {
10 | this.addVectors(this, otherVector);
11 | return this;
12 | }
13 |
14 | addVectors(v1: VectorLike, v2: VectorLike) {
15 | this.x = v1.x + v2.x;
16 | this.y = v1.y + v2.y;
17 | this.z = v1.z + v2.z;
18 | return this;
19 | }
20 |
21 | set(x?: number | VectorLike, y?: number, z?: number): EnhancedDOMPoint {
22 | if (x && typeof x === 'object') {
23 | y = x.y;
24 | z = x.z;
25 | x = x.x;
26 | }
27 | this.x = x ?? this.x;
28 | this.y = y ?? this.y;
29 | this.z = z ?? this.z;
30 | return this;
31 | }
32 |
33 | clone_() {
34 | return new EnhancedDOMPoint(this.x, this.y, this.z, this.w);
35 | }
36 |
37 | scale_(scaleBy: number) {
38 | this.x *= scaleBy;
39 | this.y *= scaleBy;
40 | this.z *= scaleBy;
41 | return this;
42 | }
43 |
44 | subtract(otherVector: VectorLike) {
45 | this.subtractVectors(this, otherVector);
46 | return this;
47 | }
48 |
49 | subtractVectors(v1: VectorLike, v2: VectorLike) {
50 | this.x = v1.x - v2.x;
51 | this.y = v1.y - v2.y;
52 | this.z = v1.z - v2.z;
53 | return this;
54 | }
55 |
56 | crossVectors(v1: EnhancedDOMPoint, v2: EnhancedDOMPoint) {
57 | const x = v1.y * v2.z - v1.z * v2.y;
58 | const y = v1.z * v2.x - v1.x * v2.z;
59 | const z = v1.x * v2.y - v1.y * v2.x;
60 | this.x = x
61 | this.y = y
62 | this.z = z
63 | return this;
64 | }
65 |
66 | dot(otherVector: VectorLike): number {
67 | return this.x * otherVector.x + this.y * otherVector.y + this.z * otherVector.z;
68 | }
69 |
70 | toArray() {
71 | return [this.x, this.y, this.z];
72 | }
73 |
74 | get magnitude() {
75 | return Math.hypot(...this.toArray());
76 | }
77 |
78 | normalize_() {
79 | const magnitude = this.magnitude;
80 | if (magnitude === 0) {
81 | return new EnhancedDOMPoint();
82 | }
83 | this.x /= magnitude;
84 | this.y /= magnitude;
85 | this.z /= magnitude;
86 | return this;
87 | }
88 |
89 | moveTowards(otherVector: EnhancedDOMPoint, speed: number) {
90 | const distance = new EnhancedDOMPoint().subtractVectors(otherVector, this);
91 |
92 | if (distance.magnitude > 2) {
93 | const direction_ = distance.normalize_().scale_(speed);
94 | this.add_(direction_);
95 | }
96 |
97 | return this;
98 | }
99 |
100 | isEqualTo(otherVector: EnhancedDOMPoint): boolean {
101 | return this.x === otherVector.x && this.y === otherVector.y && this.z === otherVector.z;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/modeling/building-blocks.ts:
--------------------------------------------------------------------------------
1 | import { doTimes } from '@/engine/helpers';
2 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry';
3 |
4 | export const mergeCubes = (cubes: MoldableCubeGeometry[]) => cubes.reduce((acc, curr) => acc.merge(curr));
5 |
6 | export class SegmentedWall extends MoldableCubeGeometry {
7 | totalWidth = 0;
8 | constructor(segmentWidths: number[], segmentHeight: number, topSegments: number[], bottomSegments: number[], startingX = 0,startingY = 0, depth = 2, segmentTop = false, segmentBottom = false) {
9 | let runningSide = 0;
10 | let runningLeft = 0;
11 |
12 | super(segmentWidths[0], topSegments[0], depth, segmentTop ? 6 : 1, 1, 1, 6);
13 | this.translate_(0, segmentHeight - topSegments[0] / 2 + startingY).spreadTextureCoords();
14 |
15 | topSegments.forEach((top, index) => {
16 | const currentWidth = segmentWidths.length === 1 ? segmentWidths[0] : segmentWidths[index];
17 | if (index > 0 && top > 0) {
18 | this.merge(new MoldableCubeGeometry(currentWidth, top, depth,segmentTop ? 6 : 1,1,1,6).translate_(startingX + runningSide + (currentWidth / 2), segmentHeight - top / 2 + startingY).spreadTextureCoords());
19 | }
20 |
21 | if (bottomSegments[index] > 0) {
22 | this.merge(new MoldableCubeGeometry(currentWidth, bottomSegments[index], depth, segmentBottom ? 6 : 1, 1, 1, 6).translate_(startingX + runningSide + (currentWidth / 2), bottomSegments[index] / 2 + startingY).spreadTextureCoords());
23 | }
24 | runningSide+= (index === 0 ? currentWidth / 2 : currentWidth);
25 | runningLeft += currentWidth;
26 | });
27 |
28 | this.totalWidth = runningLeft;
29 | this.all_().translate_((segmentWidths[0] - runningLeft) / 2, 0).computeNormals().done_();
30 | }
31 | }
32 |
33 | export function createHallway(frontWall: SegmentedWall, backWall: MoldableCubeGeometry, spacing: number) {
34 | return frontWall.translate_(0, 0, spacing).merge(backWall.translate_(0, 0, -spacing)).done_();
35 | }
36 |
37 | export function createBox(frontWall: SegmentedWall, backWall: SegmentedWall, leftWall: SegmentedWall, rightWall: SegmentedWall) {
38 | return createHallway(frontWall, backWall, (leftWall.totalWidth + 2) / 2)
39 | .merge(createHallway(leftWall, rightWall, (frontWall.totalWidth - 2) / 2).rotate_(0, Math.PI / 2)).computeNormals().done_();
40 | }
41 |
42 |
43 | // TODO: Remove this if i stick with ramps only
44 | export function createStairs(stepCount: number, startingHeight = 0) {
45 | const stepHeight = 1;
46 | return mergeCubes(doTimes(stepCount, index => {
47 | const currentHeight = index * stepHeight + stepHeight + startingHeight;
48 | return new MoldableCubeGeometry(1, currentHeight, 3).translate_(index, currentHeight/2);
49 | })).done_();
50 | }
51 |
52 | export function patternFill(pattern: number[], times: number) {
53 | return doTimes(times, (index) => pattern[index % pattern.length]);
54 | }
55 |
--------------------------------------------------------------------------------
/src/engine/physics/surface-collision.ts:
--------------------------------------------------------------------------------
1 | import { Face } from './face';
2 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point";
3 |
4 |
5 | // TODO: simple optimization would be to sort floor faces first, as long as there are no moving floor pieces they
6 | // could be pre sorted
7 | export function findFloorHeightAtPosition(floorFaces: Face[], positionPoint: EnhancedDOMPoint) {
8 | let height: number;
9 | const collisions = [];
10 |
11 | for (const floor of floorFaces) {
12 | const { x: x1, z: z1 } = floor.points[0];
13 | const { x: x2, z: z2 } = floor.points[1];
14 | const { x: x3, z: z3 } = floor.points[2];
15 |
16 | if ((z1 - positionPoint.z) * (x2 - x1) - (x1 - positionPoint.x) * (z2 - z1) < 0) {
17 | continue;
18 | }
19 |
20 | if ((z2 - positionPoint.z) * (x3 - x2) - (x2 - positionPoint.x) * (z3 - z2) < 0) {
21 | continue;
22 | }
23 |
24 | if ((z3 - positionPoint.z) * (x1 - x3) - (x3 - positionPoint.x) * (z1 - z3) < 0) {
25 | continue;
26 | }
27 |
28 | height = -(positionPoint.x * floor.normal.x + floor.normal.z * positionPoint.z + floor.originOffset) / floor.normal.y;
29 |
30 | const buffer = -3; // original mario 64 code uses a 78 unit buffer, but mario is 160 units tall compared to our presently much smaller sizes
31 | if (positionPoint.y - (height + buffer) < 0) {
32 | continue;
33 | }
34 |
35 | collisions.push({ height, floor });
36 | }
37 |
38 | return collisions.sort((a, b) => b.height - a.height)[0];
39 | }
40 |
41 | export function findWallCollisionsFromList(walls: Face[], position: EnhancedDOMPoint, offsetY: number, radius: number) {
42 | const collisionData = {
43 | xPush: 0,
44 | zPush: 0,
45 | walls: [] as Face[],
46 | numberOfWallsHit: 0,
47 | };
48 |
49 | const { x, z } = position;
50 | const y = position.y + offsetY;
51 |
52 | for (const wall of walls) {
53 | if (y < wall.lowerY || y > wall.upperY) {
54 | continue;
55 | }
56 |
57 | const offset = wall.normal.dot(position) + wall.originOffset;
58 | if (offset < -radius || offset > radius) {
59 | continue;
60 | }
61 |
62 | const isXProjection = wall.normal.x < -0.707 || wall.normal.x > 0.707;
63 | const w = isXProjection ? -z : x;
64 | const wNormal = isXProjection ? wall.normal.x : wall.normal.z;
65 |
66 | let w1 = -wall.points[0].z;
67 | let w2 = -wall.points[1].z;
68 | let w3 = -wall.points[2].z;
69 |
70 | if (!isXProjection) {
71 | w1 = wall.points[0].x;
72 | w2 = wall.points[1].x;
73 | w3 = wall.points[2].x;
74 | }
75 | let y1 = wall.points[0].y;
76 | let y2 = wall.points[1].y;
77 | let y3 = wall.points[2].y;
78 |
79 | const invertSign = wNormal > 0 ? 1 : -1;
80 |
81 | if (((y1 - y) * (w2 - w1) - (w1 - w) * (y2 - y1)) * invertSign > 0) {
82 | continue;
83 | }
84 | if (((y2 - y) * (w3 - w2) - (w2 - w) * (y3 - y2)) * invertSign > 0) {
85 | continue;
86 | }
87 | if (((y3 - y) * (w1 - w3) - (w3 - w) * (y1 - y3)) * invertSign > 0) {
88 | continue;
89 | }
90 |
91 | collisionData.xPush += wall.normal.x * (radius - offset);
92 | collisionData.zPush += wall.normal.z * (radius - offset);
93 | collisionData.walls.push(wall);
94 | collisionData.numberOfWallsHit++;
95 | }
96 | return collisionData;
97 | }
98 |
--------------------------------------------------------------------------------
/src/engine/renderer/object-3d.ts:
--------------------------------------------------------------------------------
1 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point";
2 | import { radsToDegrees } from '@/engine/helpers';
3 |
4 | export class Object3d {
5 | position_: EnhancedDOMPoint;
6 | scale_: EnhancedDOMPoint;
7 | children_: Object3d[];
8 | parent_?: Object3d;
9 | localMatrix: DOMMatrix;
10 | worldMatrix: DOMMatrix;
11 | up: EnhancedDOMPoint;
12 | rotationMatrix: DOMMatrix;
13 |
14 | constructor(...children_: Object3d[]) {
15 | this.position_ = new EnhancedDOMPoint();
16 | this.scale_ = new EnhancedDOMPoint(1, 1, 1);
17 | this.children_ = [];
18 | this.localMatrix = new DOMMatrix();
19 | this.worldMatrix = new DOMMatrix();
20 | this.up = new EnhancedDOMPoint(0, 1, 0);
21 | this.rotationMatrix = new DOMMatrix();
22 | if (children_) {
23 | this.add_(...children_);
24 | }
25 | }
26 |
27 | add_(...object3ds: Object3d[]) {
28 | object3ds.forEach(object3d => {
29 | if (object3d.parent_) {
30 | object3d.parent_.children_ = object3d.parent_.children_.filter(child => child !== this);
31 | }
32 | object3d.parent_ = this;
33 | this.children_.push(object3d);
34 | })
35 | }
36 |
37 | remove_(object3d: Object3d) {
38 | this.children_ = this.children_.filter(child => child !== object3d);
39 | }
40 |
41 | rotation_ = new EnhancedDOMPoint();
42 | rotate_(xRads: number, yRads: number, zRads: number) {
43 | this.rotation_.add_({x: radsToDegrees(xRads), y: radsToDegrees(yRads), z: radsToDegrees(zRads)});
44 | this.rotationMatrix.rotateSelf(radsToDegrees(xRads), radsToDegrees(yRads), radsToDegrees(zRads));
45 | }
46 |
47 | setRotation_(xRads: number, yRads: number, zRads: number) {
48 | this.rotationMatrix = new DOMMatrix();
49 | this.rotation_.set(radsToDegrees(xRads), radsToDegrees(yRads), radsToDegrees(zRads));
50 | this.rotationMatrix.rotateSelf(radsToDegrees(xRads), radsToDegrees(yRads), radsToDegrees(zRads));
51 | }
52 |
53 | isUsingLookAt = false;
54 | getMatrix() {
55 | const matrix = new DOMMatrix();
56 | matrix.translateSelf(this.position_.x, this.position_.y, this.position_.z);
57 | if (this.isUsingLookAt) {
58 | matrix.multiplySelf(this.rotationMatrix);
59 | } else {
60 | matrix.rotateSelf(this.rotation_.x, this.rotation_.y, this.rotation_.z);
61 | }
62 | matrix.scaleSelf(this.scale_.x, this.scale_.y, this.scale_.z);
63 | return matrix;
64 | }
65 |
66 | updateWorldMatrix() {
67 | this.localMatrix = this.getMatrix();
68 |
69 | if (this.parent_) {
70 | this.worldMatrix = this.parent_.worldMatrix.multiply(this.localMatrix);
71 | } else {
72 | this.worldMatrix = DOMMatrix.fromMatrix(this.localMatrix);
73 | }
74 |
75 | this.children_.forEach(child => child.updateWorldMatrix());
76 | }
77 |
78 | allChildren(): Object3d[] {
79 | function getChildren(object3d: Object3d, all_: Object3d[]) {
80 | object3d.children_.forEach(child => {
81 | all_.push(child);
82 | getChildren(child, all_);
83 | });
84 | }
85 |
86 | const allChildren: Object3d[] = [];
87 | getChildren(this, allChildren);
88 | return allChildren;
89 | }
90 |
91 | private right = new EnhancedDOMPoint();
92 | private lookatUp = new EnhancedDOMPoint();
93 | forward = new EnhancedDOMPoint();
94 |
95 | lookAt(target: EnhancedDOMPoint) {
96 | this.isUsingLookAt = true;
97 | this.forward.subtractVectors(this.position_, target).normalize_();
98 | this.right.crossVectors(this.up, this.forward).normalize_();
99 | this.lookatUp.crossVectors(this.forward, this.right).normalize_();
100 |
101 | this.rotationMatrix = new DOMMatrix([
102 | this.right.x, this.right.y, this.right.z, 0,
103 | this.lookatUp.x, this.lookatUp.y, this.lookatUp.z, 0,
104 | this.forward.x, this.forward.y, this.forward.z, 0,
105 | 0, 0, 0, 1,
106 | ]);
107 | }
108 | }
109 |
110 |
111 | export function createOrtho(bottom: number, top: number, left: number, right: number, near: number, far: number) {
112 | return new DOMMatrix([
113 | 2 / (right - left), 0, 0, 0,
114 | 0, 2 / (top - bottom), 0, 0,
115 | 0, 0, -2 / (far - near), 0,
116 | -(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1,
117 | ]);
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/src/game-states/menu.state.ts:
--------------------------------------------------------------------------------
1 | import { State } from '@/core/state';
2 | import { gameStateMachine } from '@/game-state-machine';
3 | import { gameStates } from '@/game-states/game-states';
4 | import { drawFullScreenText, overlaySvg } from '@/draw-helpers';
5 | import { NoiseType, text } from '@/engine/svg-maker/base';
6 | import { drawBloodText, materials, skyboxes, testHeightmap } from '@/textures';
7 | import { newNoiseLandscape } from '@/engine/new-new-noise';
8 | import { Mesh } from '@/engine/renderer/mesh';
9 | import { PlaneGeometry } from '@/engine/plane-geometry';
10 | import { castleContainer } from '@/modeling/castle';
11 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry';
12 | import { Skybox } from '@/engine/skybox';
13 | import { Scene } from '@/engine/renderer/scene';
14 | import { Camera } from '@/engine/renderer/camera';
15 | import { render } from '@/engine/renderer/renderer';
16 | import { getLeverDoors, makeBanners } from '@/modeling/items';
17 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
18 | import { makeSong, pickup1, scaryNote2 } from '@/sound-effects';
19 | import { audioCtx } from '@/engine/audio/audio-player';
20 |
21 | export class MenuState implements State {
22 | camera: Camera;
23 |
24 | scene: Scene;
25 |
26 |
27 |
28 |
29 |
30 | constructor() {
31 | this.camera = new Camera(Math.PI / 3, 16 / 9, 1, 600);
32 | this.camera.position_.y = 32;
33 | this.camera.position_.z = 120;
34 | this.scene = new Scene();
35 | }
36 |
37 | song = makeSong();
38 | drumHit = scaryNote2(0.6)();
39 | isSongPlaying = false;
40 |
41 | async onEnter() {
42 | const heightmap = await newNoiseLandscape(256, 6, 0.05, 3, NoiseType.Fractal, 113);
43 | const floor = new Mesh(new PlaneGeometry(1024, 1024, 255, 255, heightmap).spreadTextureCoords(), materials.grass);
44 | const castle = new Mesh(castleContainer.value!, materials.brickWall);
45 | const bridge = new Mesh(new MoldableCubeGeometry(18, 1, 65).translate_(0, 20.5, -125).done_(), materials.planks);
46 |
47 | // Banners
48 | const bannerHeightmap = await testHeightmap();
49 |
50 | this.scene.add_(floor, castle, bridge, ...getLeverDoors().flatMap(leverDoor => leverDoor.doorDatas), makeBanners(bannerHeightmap));
51 |
52 | this.scene.skybox = new Skybox(...skyboxes.test);
53 | this.scene.updateWorldMatrix();
54 | this.scene.skybox.bindGeometry();
55 |
56 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' },
57 | drawBloodText({ x: '50%', y: 300 }, 'UPYRI'),
58 | text({ x: '50%', y: 900, id_: 'Start' }, 'Start'),
59 | text({ x: '50%', y: 1010, id_: 'Fullscreen' }, 'Fullscreen'),
60 | );
61 | // TODO: Probably add this to the svg library, if I have enough space to keep it anyway
62 | tmpl.querySelectorAll('feTurbulence').forEach((el: HTMLElement) => {
63 | el.innerHTML = ``;
64 | });
65 |
66 | Start.onclick = () => {
67 | this.drumHit.stop();
68 | this.song.stop();
69 | pickup1().start();
70 | drawFullScreenText('LOADING');
71 | setTimeout(() => gameStateMachine.setState(gameStates.gameState), 10);
72 | };
73 |
74 | Fullscreen.onclick = () => {
75 | if (!document.fullscreenElement) {
76 | document.documentElement.requestFullscreen();
77 | } else {
78 | document.exitFullscreen();
79 | }
80 | };
81 |
82 | if (!this.isSongPlaying) {
83 | this.song.loop = true;
84 | this.song.start();
85 |
86 | this.drumHit.loop = true;
87 | this.drumHit.start(audioCtx.currentTime + 2);
88 | this.isSongPlaying = true;
89 | }
90 | }
91 |
92 | cameraRotationAngles = new EnhancedDOMPoint();
93 |
94 | onUpdate(): void {
95 | render(this.camera, this.scene);
96 |
97 | this.cameraRotationAngles.x -= 0.0015;
98 | this.cameraRotationAngles.y -= 0.003;
99 | this.cameraRotationAngles.z -= 0.0015;
100 |
101 | this.camera.position_.x = (Math.cos(this.cameraRotationAngles.x) * 125);
102 | this.camera.position_.y = Math.cos(this.cameraRotationAngles.y) * 35 + 50;
103 | this.camera.position_.z = (Math.sin(this.cameraRotationAngles.z) * 125);
104 |
105 | this.camera.lookAt(new EnhancedDOMPoint(0.1, 31, 0.1));
106 | this.camera.updateWorldMatrix();
107 | }
108 |
109 | onLeave() {
110 | this.scene = undefined;
111 | this.camera = undefined;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/modeling/lever-door.ts:
--------------------------------------------------------------------------------
1 | import { Object3d } from '@/engine/renderer/object-3d';
2 | import { Mesh } from '@/engine/renderer/mesh';
3 | import { Material } from '@/engine/renderer/material';
4 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
5 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry';
6 | import { Face } from '@/engine/physics/face';
7 | import { getGroupedFaces, meshToFaces } from '@/engine/physics/parse-faces';
8 | import {
9 | doorCreak,
10 | draggingSound2,
11 | draggingSound4,
12 | } from '@/sound-effects';
13 | import { materials } from "@/textures";
14 |
15 | export class DoorData extends Object3d {
16 | swapHingeSideX: -1 | 1;
17 | swapHingeSideZ: -1 | 1;
18 | closedDoorCollisionM: Mesh;
19 | openDoorCollisionM: Mesh;
20 | dragPlayer: AudioBufferSourceNode;
21 | creakPlayer: AudioBufferSourceNode;
22 | originalRot = 0;
23 |
24 | constructor(doorMesh: Mesh, position_: EnhancedDOMPoint, swapHingeSideX: 1 | -1 = 1, swapHingeSideZ: 1 | -1 = 1, swapOpenClosed?: boolean, isMainGate?: boolean) {
25 | super(doorMesh);
26 | this.swapHingeSideX = swapHingeSideX;
27 | this.swapHingeSideZ = swapHingeSideZ;
28 |
29 | this.position_.set(position_.x - 2 * swapHingeSideX, position_.y, position_.z);
30 | this.children_[0].position_.x = 2 * swapHingeSideX;
31 |
32 | this.closedDoorCollisionM = new Mesh(
33 | new MoldableCubeGeometry(isMainGate ? 6 : 4, 7, 1)
34 | .translate_(position_.x - (swapOpenClosed ? 4 : 0), position_.y, position_.z)
35 | .done_()
36 | , new Material());
37 |
38 | this.openDoorCollisionM = new Mesh(
39 | new MoldableCubeGeometry(4, 7, 1)
40 | .rotate_(0, Math.PI / 2)
41 | .translate_(position_.x - 2 * swapHingeSideX, position_.y, position_.z - 2 * swapHingeSideZ)
42 | .done_()
43 | , new Material());
44 |
45 | if (swapOpenClosed) {
46 | const temp = this.closedDoorCollisionM;
47 | this.closedDoorCollisionM = this.openDoorCollisionM;
48 | this.openDoorCollisionM = temp;
49 | this.rotation_.y = 90;
50 | this.originalRot = 90;
51 | }
52 |
53 | this.dragPlayer = draggingSound4(position_);
54 | this.creakPlayer = doorCreak(position_);
55 | }
56 | }
57 |
58 | export class LeverDoorObject3d extends Object3d {
59 | doorDatas: DoorData[] = [];
60 | isPulled = false;
61 | isFinished = false;
62 | switchPosition: EnhancedDOMPoint;
63 | closedDoorCollisionMs: Mesh[];
64 | openDoorCollisionMs: Mesh[];
65 | closedDoorCollision: Face[];
66 | openDoorCollision: Face[];
67 | audioPlayer: AudioBufferSourceNode;
68 |
69 |
70 | constructor(switchPosition: EnhancedDOMPoint, doorDatas: DoorData[], switchRotationDegrees = 0) {
71 | const base = new Mesh(new MoldableCubeGeometry(1, 2, 1).spreadTextureCoords(), materials.iron);
72 | const lever = new Mesh(
73 | new MoldableCubeGeometry(1, 1, 4, 3, 3)
74 | .cylindrify(0.25, 'z')
75 | .merge(new MoldableCubeGeometry(3, 1, 1, 1, 3, 3).cylindrify(0.25, 'x').translate_(0, 0, 2))
76 | .computeNormals(true)
77 | .done_(), materials.wood);
78 | super(base, lever);
79 |
80 | this.doorDatas = doorDatas;
81 | this.switchPosition = switchPosition;
82 | this.position_.set(switchPosition);
83 | this.rotation_.y = switchRotationDegrees;
84 |
85 | lever.rotation_.x = -45;
86 |
87 | this.closedDoorCollisionMs = doorDatas.flatMap(door => door.closedDoorCollisionM);
88 | this.openDoorCollisionMs = doorDatas.flatMap(door => door.openDoorCollisionM);
89 |
90 | this.closedDoorCollision = getGroupedFaces(meshToFaces(this.closedDoorCollisionMs)).wallFaces;
91 | this.openDoorCollision = getGroupedFaces(meshToFaces(this.openDoorCollisionMs)).wallFaces;
92 |
93 | this.audioPlayer = draggingSound2(switchPosition);
94 | }
95 |
96 | pullLever() {
97 | this.isPulled = true;
98 | this.audioPlayer.start();
99 | this.doorDatas.forEach(door => {
100 | door.dragPlayer.start();
101 | door.creakPlayer.start();
102 | });
103 | }
104 |
105 | update(){
106 | if (this.isPulled && !this.isFinished) {
107 | this.doorDatas.forEach(door => {
108 | door.rotation_.y += door.swapHingeSideZ * door.swapHingeSideX * 0.6;
109 | this.children_[1].rotation_.x += (1/this.doorDatas.length) * 0.6;
110 | if (Math.abs(door.rotation_.y) - door.originalRot >= 90) {
111 | this.isFinished = true;
112 | }
113 | });
114 | }
115 | }
116 | }
117 |
118 |
--------------------------------------------------------------------------------
/src/core/first-person-player.ts:
--------------------------------------------------------------------------------
1 | import { Camera } from '@/engine/renderer/camera';
2 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
3 | import { Face } from '@/engine/physics/face';
4 | import { controls } from '@/core/controls';
5 | import {
6 | findFloorHeightAtPosition,
7 | findWallCollisionsFromList,
8 | } from '@/engine/physics/surface-collision';
9 | import { audioCtx } from '@/engine/audio/audio-player';
10 | import { clamp } from '@/engine/helpers';
11 | import { indoorFootsteps, outsideFootsteps } from '@/sound-effects';
12 |
13 |
14 | export class FirstPersonPlayer {
15 | isJumping = false;
16 | feetCenter = new EnhancedDOMPoint(0, 0, 0);
17 | velocity = new EnhancedDOMPoint(0, 0, 0);
18 | isFrozen = false;
19 |
20 | // mesh: Mesh;
21 | camera: Camera;
22 | cameraRotation = new EnhancedDOMPoint(0, 0, 0);
23 | listener: AudioListener;
24 | footstepsPlayer;
25 | isOnDirt = true;
26 |
27 | constructor(camera: Camera) {
28 | this.feetCenter.set(44, 21, -26);
29 | this.camera = camera;
30 | this.listener = audioCtx.listener;
31 |
32 | this.footstepsPlayer = outsideFootsteps();
33 | this.footstepsPlayer.loop = true;
34 | this.footstepsPlayer.playbackRate.value = 0;
35 | this.footstepsPlayer.start();
36 |
37 | const rotationSpeed = 0.002;
38 | controls.onMouseMove(mouseMovement => {
39 | this.cameraRotation.x += mouseMovement.y * -rotationSpeed;
40 | this.cameraRotation.y += mouseMovement.x * -rotationSpeed;
41 | this.cameraRotation.x = clamp(this.cameraRotation.x, -Math.PI / 2, Math.PI / 2);
42 | this.cameraRotation.y = this.cameraRotation.y % (Math.PI * 2);
43 | });
44 | }
45 |
46 | private isFootstepsStopped = true;
47 |
48 | update(gridFaces: {floorFaces: Face[], wallFaces: Face[]}[]) {
49 | //debug.innerHTML = this.feetCenter.y;
50 | if (!this.isFrozen) {
51 | this.updateVelocityFromControls();
52 | }
53 |
54 | if (!this.isJumping && this.velocity.magnitude > 0) {
55 | if (this.isFootstepsStopped) {
56 | this.footstepsPlayer.stop();
57 | this.footstepsPlayer = this.determineFootstepPlayer(this.feetCenter.y);
58 | this.footstepsPlayer.loop = true;
59 | this.footstepsPlayer.start();
60 | this.isFootstepsStopped = false;
61 | }
62 | } else {
63 | this.isFootstepsStopped = true;
64 | this.footstepsPlayer.loop = false;
65 | }
66 |
67 | this.velocity.y -= 0.003; // gravity
68 | this.feetCenter.add_(this.velocity);
69 |
70 | const playerGridPosition = this.feetCenter.x < 0 ? 0 : 1;
71 |
72 |
73 | // @ts-ignore
74 | this.collideWithLevel(gridFaces[playerGridPosition]); // do collision detection, if collision is found, feetCenter gets pushed out of the collision
75 |
76 | this.camera.position_.set(this.feetCenter);
77 | this.camera.position_.y += 3.5;
78 |
79 |
80 | // @ts-ignore
81 | this.camera.setRotation_(...this.cameraRotation.toArray());
82 |
83 | this.camera.updateWorldMatrix();
84 |
85 | this.updateAudio();
86 | }
87 |
88 | wallCollision(wallFaces: Face[]) {
89 | const wallCollisions = findWallCollisionsFromList(wallFaces, this.feetCenter, 1.1, 1.5);
90 | this.feetCenter.x += wallCollisions.xPush;
91 | this.feetCenter.z += wallCollisions.zPush;
92 | if (wallCollisions.numberOfWallsHit > 0) {
93 | this.velocity.x = 0;
94 | this.velocity.z = 0;
95 | }
96 | }
97 |
98 | collideWithLevel(groupedFaces: {floorFaces: Face[], wallFaces: Face[]}) {
99 | this.wallCollision(groupedFaces.wallFaces);
100 |
101 | const floorData = findFloorHeightAtPosition(groupedFaces!.floorFaces, this.feetCenter);
102 | if (!floorData) {
103 | this.isJumping = true;
104 | return;
105 | }
106 |
107 | const collisionDepth = floorData.height - this.feetCenter.y;
108 |
109 | if (collisionDepth > 0) {
110 | this.feetCenter.y += collisionDepth;
111 | this.velocity.y = 0;
112 |
113 | if (this.isOnDirt && floorData.height > 21) {
114 | this.footstepsPlayer.stop();
115 | this.footstepsPlayer = this.determineFootstepPlayer(floorData.height);
116 | this.footstepsPlayer.loop = true;
117 | this.footstepsPlayer.start();
118 | this.isOnDirt = false;
119 | }
120 |
121 | if (!this.isOnDirt && floorData.height === 21) {
122 | this.footstepsPlayer.stop();
123 | this.footstepsPlayer = this.determineFootstepPlayer(floorData.height);
124 | this.footstepsPlayer.loop = true;
125 | this.footstepsPlayer.start();
126 | this.isOnDirt = true;
127 | }
128 |
129 | this.isJumping = false;
130 | } else {
131 | this.isJumping = true;
132 | }
133 | }
134 |
135 | determineFootstepPlayer(height_: number) {
136 | if (height_ === 21) {
137 | return outsideFootsteps();
138 | } else {
139 | return indoorFootsteps();
140 | }
141 | }
142 |
143 | protected updateVelocityFromControls() {
144 | const speed = 0.18;
145 |
146 | const depthMovementZ = Math.cos(this.cameraRotation.y) * controls.inputDirection.y;
147 | const depthMovementX = Math.sin(this.cameraRotation.y) * controls.inputDirection.y;
148 |
149 | const sidestepZ = Math.cos(this.cameraRotation.y + Math.PI / 2) * controls.inputDirection.x;
150 | const sidestepX = Math.sin(this.cameraRotation.y + Math.PI / 2) * controls.inputDirection.x;
151 |
152 | this.velocity.z = depthMovementZ + sidestepZ;
153 | this.velocity.x = depthMovementX + sidestepX;
154 | let oldY = this.velocity.y;
155 | this.velocity.normalize_().scale_(speed);
156 | this.velocity.y = oldY;
157 | }
158 |
159 | private updateAudio() {
160 | if (this.listener.positionX) {
161 | this.listener.positionX.value = this.camera.position_.x;
162 | this.listener.positionY.value = this.camera.position_.y;
163 | this.listener.positionZ.value = this.camera.position_.z;
164 |
165 | const lookingDirection = new EnhancedDOMPoint(0, 0, -1);
166 | const result_ = this.camera.rotationMatrix.transformPoint(lookingDirection);
167 |
168 | this.listener.forwardX.value = result_.x;
169 | this.listener.forwardY.value = result_.y;
170 | this.listener.forwardZ.value = result_.z;
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["tools"],
3 | "include": ["./src"],
4 | "compilerOptions": {
5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
6 |
7 | /* Basic Options */
8 | // "incremental": true, /* Enable incremental compilation */
9 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
10 | "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
11 | // "lib": [], /* Specify library files to be included in the compilation. */
12 | // "allowJs": true, /* Allow javascript files to be compiled. */
13 | // "checkJs": true, /* Report errors in .js files. */
14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
15 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
17 | // "sourceMap": true, /* Generates corresponding '.map' file. */
18 | // "outFile": "./", /* Concatenate and emit output to single file. */
19 | "outDir": "./dist", /* Redirect output structure to the directory. */
20 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
21 | // "composite": true, /* Enable project compilation */
22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
23 | // "removeComments": true, /* Do not emit comments to output. */
24 | // "noEmit": true, /* Do not emit outputs. */
25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
28 |
29 | /* Strict Type-Checking Options */
30 | "strict": true, /* Enable all strict type-checking options. */
31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
32 | // "strictNullChecks": true, /* Enable strict null checks. */
33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
38 |
39 | /* Additional Checks */
40 | // "noUnusedLocals": true, /* Report errors on unused locals. */
41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
44 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
45 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
46 |
47 | /* Module Resolution Options */
48 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
49 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
50 | "paths": {
51 | "@/*": ["src/*"]
52 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
53 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
54 | // "typeRoots": [], /* List of folders to include type definitions from. */
55 | // "types": [], /* Type declaration files to be included in compilation. */
56 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
57 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
59 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
60 |
61 | /* Source Map Options */
62 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
64 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
65 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
66 |
67 | /* Experimental Options */
68 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
69 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
70 |
71 | /* Advanced Options */
72 | "skipLibCheck": true, /* Skip type checking of declaration files. */
73 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/engine/renderer/renderer.ts:
--------------------------------------------------------------------------------
1 | import { gl, lilgl } from "@/engine/renderer/lil-gl";
2 | import { Camera } from "@/engine/renderer/camera";
3 | import { Skybox } from '@/engine/skybox';
4 |
5 | import { Scene } from '@/engine/renderer/scene';
6 | import { Mesh } from '@/engine/renderer/mesh';
7 | import {
8 | emissive, lightPovMvp,
9 | modelviewProjection,
10 | normalMatrix,
11 | textureRepeat, u_skybox, u_viewDirectionProjectionInverse
12 | } from '@/engine/shaders/shaders';
13 | import { createOrtho, Object3d } from '@/engine/renderer/object-3d';
14 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
15 |
16 | // IMPORTANT! The index of a given buffer in the buffer array must match it's respective data location in the shader.
17 | // This allows us to use the index while looping through buffers to bind the attributes. So setting a buffer
18 | // happens by placing
19 | export const enum AttributeLocation {
20 | Positions,
21 | Normals,
22 | TextureCoords,
23 | TextureDepth,
24 | LocalMatrix,
25 | NormalMatrix = 8,
26 | }
27 |
28 | gl.enable(gl.CULL_FACE);
29 | gl.enable(gl.DEPTH_TEST);
30 | gl.enable(gl.BLEND);
31 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
32 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
33 | const modelviewProjectionLocation = gl.getUniformLocation(lilgl.program, modelviewProjection)!;
34 | const normalMatrixLocation = gl.getUniformLocation(lilgl.program, normalMatrix)!;
35 | const emissiveLocation = gl.getUniformLocation(lilgl.program, emissive)!;
36 | const textureRepeatLocation = gl.getUniformLocation(lilgl.program, textureRepeat)!;
37 | const skyboxLocation = gl.getUniformLocation(lilgl.skyboxProgram, u_skybox)!;
38 | const viewDirectionProjectionInverseLocation = gl.getUniformLocation(lilgl.skyboxProgram, u_viewDirectionProjectionInverse)!;
39 |
40 |
41 | const origin = new EnhancedDOMPoint(0, 0, 0);
42 |
43 | const lightPovProjection = createOrtho(-105,105,-105,105,-400,400);
44 |
45 | const inverseLightDirection = new EnhancedDOMPoint(-0.8, 1.5, -1).normalize_();
46 | const lightPovView = new Object3d();
47 | lightPovView.position_.set(inverseLightDirection);
48 | lightPovView.lookAt(origin);
49 | lightPovView.rotationMatrix.invertSelf();
50 |
51 | const lightPovMvpMatrix = lightPovProjection.multiply(lightPovView.rotationMatrix);
52 |
53 | const lightPovMvpDepthLocation = gl.getUniformLocation(lilgl.depthProgram, lightPovMvp);
54 | gl.useProgram(lilgl.depthProgram);
55 | gl.uniformMatrix4fv(lightPovMvpDepthLocation, false, lightPovMvpMatrix.toFloat32Array());
56 |
57 | const textureSpaceConversion = new DOMMatrix([
58 | 0.5, 0.0, 0.0, 0.0,
59 | 0.0, 0.5, 0.0, 0.0,
60 | 0.0, 0.0, 0.5, 0.0,
61 | 0.5, 0.5, 0.5, 1.0
62 | ]);
63 |
64 | const textureSpaceMvp = textureSpaceConversion.multiplySelf(lightPovMvpMatrix);
65 | const lightPovMvpRenderLocation = gl.getUniformLocation(lilgl.program, lightPovMvp);
66 | gl.useProgram(lilgl.program);
67 | gl.uniformMatrix4fv(lightPovMvpRenderLocation, false, textureSpaceMvp.toFloat32Array());
68 |
69 | const depthTextureSize = new DOMPoint(4096, 4096);
70 | const depthTexture = gl.createTexture();
71 | gl.activeTexture(gl.TEXTURE1);
72 | gl.bindTexture(gl.TEXTURE_2D, depthTexture);
73 | gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT32F, depthTextureSize.x, depthTextureSize.y);
74 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE);
75 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
76 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
77 |
78 | const depthFramebuffer = gl.createFramebuffer();
79 | gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer);
80 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
81 |
82 |
83 | export function render(camera: Camera, scene: Scene) {
84 | const viewMatrix = camera.worldMatrix.inverse();
85 | const viewMatrixCopy = viewMatrix.scale(1, 1, 1);
86 | const viewProjectionMatrix = camera.projection.multiply(viewMatrix);
87 |
88 | const renderSkybox = (skybox: Skybox) => {
89 | gl.useProgram(lilgl.skyboxProgram);
90 | gl.uniform1i(skyboxLocation, 0);
91 | viewMatrixCopy.m41 = 0;
92 | viewMatrixCopy.m42 = 0;
93 | viewMatrixCopy.m43 = 0;
94 | const inverseViewProjection = camera.projection.multiply(viewMatrixCopy).inverse();
95 | gl.uniformMatrix4fv(viewDirectionProjectionInverseLocation, false, inverseViewProjection.toFloat32Array());
96 | gl.bindVertexArray(skybox.vao);
97 | gl.drawArrays(gl.TRIANGLES, 0, 6);
98 | };
99 |
100 | const renderMesh = (mesh: Mesh, projection: DOMMatrix) => {
101 | // @ts-ignore
102 | gl.useProgram(lilgl.program);
103 | gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
104 | const modelViewProjectionMatrix = projection.multiply(mesh.worldMatrix);
105 |
106 | gl.uniform4fv(emissiveLocation, mesh.material.emissive);
107 | gl.vertexAttrib1f(AttributeLocation.TextureDepth, mesh.material.texture?.id ?? -1.0);
108 | const textureRepeat = [mesh.material.texture?.textureRepeat.x ?? 1, mesh.material.texture?.textureRepeat.y ?? 1];
109 | gl.uniform2fv(textureRepeatLocation, textureRepeat);
110 |
111 | gl.bindVertexArray(mesh.geometry.vao!);
112 |
113 | // @ts-ignore
114 | gl.uniformMatrix4fv(normalMatrixLocation, true, mesh.color ? mesh.cachedMatrixData : mesh.worldMatrix.inverse().toFloat32Array());
115 | gl.uniformMatrix4fv(modelviewProjectionLocation, false, modelViewProjectionMatrix.toFloat32Array());
116 | if (!mesh.material.isTransparent) {
117 | gl.uniformMatrix4fv(lightPovMvpRenderLocation, false, textureSpaceMvp.multiply(mesh.worldMatrix).toFloat32Array());
118 | }
119 | gl.drawElements(gl.TRIANGLES, mesh.geometry.getIndices()!.length, gl.UNSIGNED_SHORT, 0);
120 | };
121 |
122 |
123 | // Render shadow map to depth texture
124 | gl.useProgram(lilgl.depthProgram);
125 | // gl.cullFace(gl.FRONT);
126 | gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer);
127 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
128 | gl.viewport(0, 0, depthTextureSize.x, depthTextureSize.y);
129 |
130 | scene.solidMeshes.forEach((mesh, index) => {
131 | if (index > 0) {
132 | gl.bindVertexArray(mesh.geometry.vao!);
133 | gl.uniformMatrix4fv(lightPovMvpDepthLocation, false, lightPovMvpMatrix.multiply(mesh.worldMatrix).toFloat32Array());
134 | gl.drawElements(gl.TRIANGLES, mesh.geometry.getIndices()!.length, gl.UNSIGNED_SHORT, 0);
135 | }
136 | });
137 |
138 | // Render solid meshes first
139 | // gl.activeTexture(gl.TEXTURE0);
140 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
141 | gl.bindFramebuffer(gl.FRAMEBUFFER, null);
142 | gl.cullFace(gl.BACK);
143 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
144 | scene.solidMeshes.forEach(mesh => renderMesh(mesh, viewProjectionMatrix));
145 |
146 | // Set the depthFunc to less than or equal so the skybox can be drawn at the absolute farthest depth. Without
147 | // this the skybox will be at the draw distance and so not drawn. After drawing set this back.
148 | if (scene.skybox) {
149 | gl.depthFunc(gl.LEQUAL);
150 | renderSkybox(scene.skybox!);
151 | gl.depthFunc(gl.LESS);
152 | }
153 |
154 | // Now render transparent items. For transparent items, stop writing to the depth mask. If we don't do this
155 | // the transparent portion of a transparent mesh will hide other transparent items. After rendering the
156 | // transparent items, set the depth mask back to writable.
157 | gl.depthMask(false);
158 | scene.transparentMeshes.forEach(mesh => renderMesh(mesh, viewProjectionMatrix));
159 | gl.depthMask(true);
160 |
161 | // Unbinding the vertex array being used to make sure the last item drawn isn't still bound on the next draw call.
162 | // In theory this isn't necessary but avoids bugs.
163 | gl.bindVertexArray(null);
164 | }
165 |
--------------------------------------------------------------------------------
/src/modeling/items.ts:
--------------------------------------------------------------------------------
1 | import { getTextureForSide, MoldableCubeGeometry } from '@/engine/moldable-cube-geometry';
2 | import { Mesh } from '@/engine/renderer/mesh';
3 | import { Material } from '@/engine/renderer/material';
4 | import { SegmentedWall } from '@/modeling/building-blocks';
5 | import { materials } from "@/textures";
6 | import { AttributeLocation } from "@/engine/renderer/renderer";
7 | import { DoorData, LeverDoorObject3d } from '@/modeling/lever-door';
8 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point';
9 | import { PlaneGeometry } from '@/engine/plane-geometry';
10 |
11 | export function stake() {
12 | return new Mesh(new MoldableCubeGeometry(0.5, 0.5, 2, 2, 2)
13 | .selectBy(vert => vert.z > 0 && vert.x === 0)
14 | .translate_(0, 0, 1)
15 | .scale_(1, 0, 1)
16 | .all_()
17 | .spreadTextureCoords()
18 | .done_(), materials.wood);
19 | }
20 |
21 | export function key() {
22 | return new Mesh(
23 | new MoldableCubeGeometry(1, 1, 0.5, 1, 3)
24 | .cylindrify(1, 'z')
25 | .merge(new SegmentedWall([1, 0.5, 0.5, 0.5], 1, [0.25, 1, 0.25, 1], [0], 0, 0, 0.5).translate_(2, -0.75))
26 | .scale_(0.5, 0.5, 0.5)
27 | .rotate_(0, Math.PI / 2)
28 | .translate_(-32,36,60)
29 | .done_(),
30 | materials.gold);
31 | }
32 |
33 | export function upyri() {
34 | const fang = () => new MoldableCubeGeometry(2, 0.5, 2, 3, 1, 3)
35 | .cylindrify(0.08)
36 | .selectBy(vert => vert.y > 0)
37 | .scale_(0, 1, 0)
38 | .all_()
39 | .translate_(0, 0.3, 0.53)
40 | .rotate_(0.2)
41 | .setAttribute_(AttributeLocation.TextureDepth, new Float32Array(getTextureForSide(9, 9, materials.silver.texture!)), 1);
42 |
43 | const obj = new Mesh(new MoldableCubeGeometry(1, 1, 1, 6, 6, 6)
44 | .spherify(0.8)
45 | .scale_(1, 1.3, 0.8)
46 | .selectBy(vert => vert.y > 0.7)
47 | .scale_(3, 1, 1)
48 | .selectBy(vert => vert.y > 0.8)
49 | .scale_(1.5, 8, 2)
50 | .setAttribute_(AttributeLocation.TextureDepth, new Float32Array(MoldableCubeGeometry.TexturePerSide(6, 6, 6,
51 | materials.iron.texture!,
52 | materials.iron.texture!,
53 | materials.iron.texture!,
54 | materials.iron.texture!,
55 | materials.face.texture!,
56 | materials.face.texture!,
57 | )), 1)
58 | .merge(fang().translate_(-0.2))
59 | .merge(fang().translate_(0.2))
60 | .computeNormals(true)
61 | .all_()
62 | .rotate_(Math.PI)
63 | .done_()
64 | , materials.face);
65 |
66 | obj.position_.set(0, 54, 2);
67 |
68 | return obj;
69 | }
70 |
71 | export function makeCoffinBottomTop() {
72 | return new MoldableCubeGeometry(3, 0.5, 7.5, 1, 1, 2)
73 | .selectBy(vertex => vertex.z === 0)
74 | .translate_(0, 0, -1)
75 | .scale_(1.5)
76 | .all_()
77 | .translate_(0, -0.2, 9)
78 | .spreadTextureCoords();
79 | }
80 |
81 | export function makeCoffin() {
82 | function makeCoffinSide(swap = 1) {
83 | return new MoldableCubeGeometry(0.5, 2, 7.5, 1, 1, 2)
84 | .selectBy(vertex => vertex.z === 0)
85 | .translate_(swap, 0, -1)
86 | .all_()
87 | .translate_(1.5 * swap, 0.4, 9)
88 | .spreadTextureCoords();
89 | }
90 |
91 | function makeCoffinFrontBack(isSwap = false) {
92 | return new MoldableCubeGeometry(3, 2, 0.5)
93 | .selectBy(vertex => (isSwap ? -vertex.z : vertex.z) > 0)
94 | .scale_(1.17)
95 | .all_()
96 | .translate_(0, 0.4, isSwap ? 13 : 5)
97 | .spreadTextureCoords();
98 | }
99 |
100 | return makeCoffinSide(-1)
101 | .merge(makeCoffinSide())
102 | .merge(makeCoffinFrontBack())
103 | .merge(makeCoffinFrontBack(true))
104 | .merge(makeCoffinBottomTop())
105 | .computeNormals()
106 | .done_();
107 | }
108 |
109 | export function fenceDoor() {
110 | return new Mesh(
111 | new SegmentedWall([0.25, 0.5, 0.1, 0.5, 0.1, 0.5, 0.1, 0.5, 0.1, 0.5, 0.1, 0.5, 0.25], 7, [7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7], [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], 0, 0, 0.15)
112 | .translate_(0, -3.5)
113 | .done_(),
114 | materials.silver);
115 | }
116 |
117 | export function woodenDoor(hasLock = false, width_ = 4, height_ = 7) {
118 | const doorTextures = MoldableCubeGeometry.TexturePerSide(1, 1, 1,
119 | materials.wood.texture!,
120 | materials.wood.texture!,
121 | materials.wood.texture!,
122 | materials.wood.texture!,
123 | materials.planks.texture!,
124 | materials.planks.texture!,
125 | );
126 | const doorGeo = new MoldableCubeGeometry(width_, height_, 1);
127 |
128 | doorGeo.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(doorTextures), 1);
129 |
130 | const barGeo = new MoldableCubeGeometry(width_ + .05, 0.5, 1.2).translate_(0, width_ === 4 ? 2.5: 5).spreadTextureCoords();
131 | const barGeo2 = new MoldableCubeGeometry(width_ + .05, 0.5, 1.2).translate_(0, width_ === 4 ? -2.5: -1).spreadTextureCoords();
132 |
133 | const barTextures = MoldableCubeGeometry.TexturePerSide(1, 1, 1,
134 | materials.iron.texture!,
135 | materials.iron.texture!,
136 | materials.iron.texture!,
137 | materials.iron.texture!,
138 | materials.iron.texture!,
139 | materials.iron.texture!,
140 | );
141 |
142 | barGeo.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(barTextures), 1);
143 | barGeo2.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(barTextures), 1);
144 |
145 |
146 | const lock = new MoldableCubeGeometry(1, 1, 1.2).translate_(-1.4).done_();
147 |
148 | const lockTextures = MoldableCubeGeometry.TexturePerSide(1, 1, 1,
149 | materials.gold.texture!,
150 | materials.gold.texture!,
151 | materials.gold.texture!,
152 | materials.gold.texture!,
153 | materials.keyLock.texture!,
154 | materials.keyLock.texture!,
155 | );
156 |
157 | lock.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(lockTextures), 1);
158 |
159 | doorGeo.merge(barGeo).merge(barGeo2);
160 |
161 | if (hasLock) {
162 | doorGeo.merge(lock);
163 | }
164 |
165 | doorGeo.done_();
166 |
167 | return new Mesh(doorGeo, new Material());
168 | }
169 |
170 | export function getLeverDoors() {
171 | return [
172 | // Corner entrance
173 | new LeverDoorObject3d(new EnhancedDOMPoint(42, 36, -60), [
174 | new DoorData(fenceDoor(), new EnhancedDOMPoint(53, 36.5, -49))
175 | ], -90),
176 |
177 |
178 | // Keep entrance
179 | new LeverDoorObject3d(new EnhancedDOMPoint(57, 24, 42), [
180 | new DoorData(woodenDoor(), new EnhancedDOMPoint(-2, 24.5, -15)),
181 | new DoorData(woodenDoor(), new EnhancedDOMPoint(2, 24.5, -15), -1, 1),
182 | new DoorData(woodenDoor(), new EnhancedDOMPoint(53, 24.5, 47), -1, -1)
183 | ], -90),
184 |
185 | // Locked door to upper keep
186 | new LeverDoorObject3d(new EnhancedDOMPoint(23, 0, 37.5), [
187 | new DoorData(woodenDoor(true), new EnhancedDOMPoint(23, 24.5, 37.5), -1)
188 | ]),
189 |
190 |
191 | // Front gate
192 | new LeverDoorObject3d(new EnhancedDOMPoint(3, 58, -12), [
193 | new DoorData(woodenDoor(false, 6, 15), new EnhancedDOMPoint(-3, 24, -60), 1, 1, false, true),
194 | new DoorData(woodenDoor(false, 6, 15), new EnhancedDOMPoint(3, 24, -60), -1, 1, false, true)
195 | ]),
196 |
197 | // Door to key
198 | new LeverDoorObject3d(new EnhancedDOMPoint(-11, 36, 50), [
199 | new DoorData(fenceDoor(), new EnhancedDOMPoint(-25, 36, 61.5), 1, 1, true)
200 | ], -90)
201 | ];
202 | }
203 |
204 | function bannerMaker(bannerHeightmap: number[]) {
205 | const banner = new PlaneGeometry(64, 64, 31, 31, bannerHeightmap)
206 | .selectBy(vert => vert.z <= 20)
207 | .modifyEachVertex(vert => vert.z -= Math.abs(vert.x) / 3)
208 | .all_()
209 | .spreadTextureCoords(60, 12, 0.5, 0.18)
210 | .scale_(0.05, 1, 0.2)
211 | .rotate_(-Math.PI / 2)
212 | .computeNormals(true);
213 |
214 | const textureDepths = banner.vertices.map(vert => {
215 | if (vert.y < -2.6 && vert.y > -5) {
216 | return materials.bannerIcon.texture!.id;
217 | } else {
218 | return materials.banner.texture!.id;
219 | }
220 | });
221 |
222 | banner.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(textureDepths), 1);
223 |
224 | return banner;
225 | }
226 |
227 | export function makeBanners(bannerHeightmap: number[]) {
228 | return new Mesh(
229 | bannerMaker(bannerHeightmap).translate_(18, 32, -16.6)
230 | .merge(bannerMaker(bannerHeightmap).translate_(-18, 32, -16.6))
231 | .done_(),
232 | materials.banner);
233 | }
234 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, HmrContext, IndexHtmlTransformContext, Plugin } from 'vite';
2 | import path from 'path';
3 | import fs from 'fs/promises';
4 | import typescriptPlugin from '@rollup/plugin-typescript';
5 | import { OutputAsset, OutputChunk } from 'rollup';
6 | import { Input, InputAction, InputType, Packer } from 'roadroller';
7 | import CleanCSS from 'clean-css';
8 | import { statSync } from 'fs';
9 | const { execFileSync } = require('child_process');
10 | import ect from 'ect-bin';
11 | import { exec } from 'node:child_process';
12 |
13 | const htmlMinify = require('html-minifier');
14 | const tmp = require('tmp');
15 | const ClosureCompiler = require('google-closure-compiler').compiler;
16 |
17 | const shaderMinifyConfig = {
18 | shouldMinify: true,
19 | shaderDirectory: './src/engine/shaders',
20 | output: './src/engine/shaders/shaders.ts',
21 | debounce: 2000,
22 | };
23 |
24 | export default defineConfig(({ command, mode }) => {
25 | const config = {
26 | server: {
27 | port: 3001,
28 | },
29 | resolve: {
30 | alias: {
31 | '@': path.resolve(__dirname, './src'),
32 | }
33 | },
34 | plugins: [minifyShaders(shaderMinifyConfig)]
35 | };
36 |
37 | if (command === 'build') {
38 | // @ts-ignore
39 | config.esbuild = false;
40 | // @ts-ignore
41 | config.base = '';
42 | // @ts-ignore
43 | config.build = {
44 | minify: false,
45 | target: 'es2020',
46 | modulePreload: { polyfill: false },
47 | assetsInlineLimit: 800,
48 | assetsDir: '',
49 | rollupOptions: {
50 | output: {
51 | inlineDynamicImports: true,
52 | manualChunks: undefined,
53 | assetFileNames: `[name].[ext]`
54 | },
55 | }
56 | };
57 | // @ts-ignore
58 | config.plugins.push(typescriptPlugin(), closurePlugin(), roadrollerPlugin(), ectPlugin());
59 | }
60 |
61 | return config;
62 | });
63 |
64 | function closurePlugin(): Plugin {
65 | return {
66 | name: 'closure-compiler',
67 | // @ts-ignore
68 | renderChunk: applyClosure,
69 | enforce: 'post',
70 | };
71 | }
72 |
73 | async function applyClosure(js: string, chunk: any) {
74 | const tmpobj = tmp.fileSync();
75 | // replace all consts with lets to save about 50-70 bytes
76 | // ts-ignore
77 | js = js.replaceAll('const ', 'let ');
78 |
79 | await fs.writeFile(tmpobj.name, js);
80 | const closureCompiler = new ClosureCompiler({
81 | js: tmpobj.name,
82 | externs: 'externs.js',
83 | compilation_level: 'ADVANCED',
84 | language_in: 'ECMASCRIPT_2020',
85 | language_out: 'ECMASCRIPT_2020',
86 | });
87 | return new Promise((resolve, reject) => {
88 | closureCompiler.run((_exitCode: string, stdOut: string, stdErr: string) => {
89 | if (stdOut !== '') {
90 | resolve({ code: stdOut });
91 | } else if (stdErr !== '') { // only reject if stdout isn't generated
92 | reject(stdErr);
93 | return;
94 | }
95 |
96 | console.warn(stdErr); // If we make it here, there were warnings but no errors
97 | });
98 | });
99 | }
100 |
101 |
102 | function roadrollerPlugin(): Plugin {
103 | return {
104 | name: 'vite:roadroller',
105 | transformIndexHtml: {
106 | enforce: 'post',
107 | transform: async (html: string, ctx?: IndexHtmlTransformContext): Promise => {
108 | // Only use this plugin during build
109 | if (!ctx || !ctx.bundle) {
110 | return html;
111 | }
112 |
113 | const options = {
114 | includeAutoGeneratedTags: true,
115 | removeAttributeQuotes: true,
116 | removeComments: true,
117 | removeRedundantAttributes: true,
118 | removeScriptTypeAttributes: true,
119 | removeStyleLinkTypeAttributes: true,
120 | sortClassName: true,
121 | useShortDoctype: true,
122 | collapseWhitespace: true,
123 | collapseInlineTagWhitespace: true,
124 | removeEmptyAttributes: true,
125 | removeOptionalTags: true,
126 | sortAttributes: true,
127 | minifyCSS: true,
128 | };
129 |
130 | const bundleOutputs = Object.values<(OutputAsset | OutputChunk)>(ctx.bundle);
131 | const javascript = bundleOutputs.find((output) => output.fileName.endsWith('.js')) as OutputChunk;
132 | const css = bundleOutputs.find((output) => output.fileName.endsWith('.css')) as OutputAsset;
133 | const otherBundleOutputs = bundleOutputs.filter((output) => output !== javascript);
134 | if (otherBundleOutputs.length > 0) {
135 | otherBundleOutputs.forEach((output) => console.warn(`WARN Asset not inlined: ${output.fileName}`));
136 | }
137 |
138 | const cssInHtml = css ? embedCss(html, css) : html;
139 | const minifiedHtml = await htmlMinify.minify(cssInHtml, options);
140 | return embedJs(minifiedHtml, javascript);
141 | },
142 | },
143 | };
144 | }
145 |
146 | /**
147 | * Transforms the given JavaScript code into a packed version.
148 | * @param html The original HTML.
149 | * @param chunk The JavaScript output chunk from Rollup/Vite.
150 | * @returns The transformed HTML with the JavaScript embedded.
151 | */
152 | async function embedJs(html: string, chunk: OutputChunk): Promise {
153 | const scriptTagRemoved = html.replace(new RegExp(``), '');
154 | const htmlInJs = `document.write('${scriptTagRemoved}');` + chunk.code.trim();
155 |
156 | const inputs: Input[] = [
157 | {
158 | data: htmlInJs,
159 | type: 'js' as InputType,
160 | action: 'eval' as InputAction,
161 | },
162 | ];
163 |
164 | let options;
165 | if (process.env.USE_RR_CONFIG) {
166 | try {
167 | options = JSON.parse(await fs.readFile(`${__dirname}/roadroller-config.json`, 'utf-8'));
168 | } catch(error) {
169 | throw new Error('Roadroller config not found. Generate one or use the regular build option');
170 | }
171 | } else {
172 | options = { allowFreeVars: true };
173 | }
174 |
175 | const packer = new Packer(inputs, options);
176 | await Promise.all([
177 | fs.writeFile(`${path.join(__dirname, 'dist')}/output.js`, htmlInJs),
178 | packer.optimize(process.env.LEVEL_2_BUILD ? 2 : 0) // Regular builds use level 2, but rr config builds use the supplied params
179 | ]);
180 | const { firstLine, secondLine } = packer.makeDecoder();
181 | return ``;
182 | }
183 |
184 | /**
185 | * Embeds CSS into the HTML.
186 | * @param html The original HTML.
187 | * @param asset The CSS asset.
188 | * @returns The transformed HTML with the CSS embedded.
189 | */
190 | function embedCss(html: string, asset: OutputAsset): string {
191 | const reCSS = new RegExp(`]*?href="[\./]*${asset.fileName}"[^>]*?>`);
192 | const code = ``;
193 | return html.replace(reCSS, code);
194 | }
195 |
196 | /**
197 | * Creates the ECT plugin that uses Efficient-Compression-Tool to build a zip file.
198 | * @returns The ECT plugin.
199 | */
200 | function ectPlugin(): Plugin {
201 | return {
202 | name: 'vite:ect',
203 | writeBundle: async (): Promise => {
204 | try {
205 | const files = await fs.readdir('dist/');
206 | const assetFiles = files.filter(file => {
207 | return !file.includes('.js') && !file.includes('.css') && !file.includes('.html') && !file.includes('.zip') && file !== 'assets';
208 | }).map(file => 'dist/' + file);
209 | const args = ['-strip', '-zip', '-10009', 'dist/index.html', ...assetFiles];
210 | const result = execFileSync(ect, args);
211 | console.log('ECT result', result.toString().trim());
212 | const stats = statSync('dist/index.zip');
213 | console.log('ZIP size', stats.size);
214 | } catch (err) {
215 | console.log('ECT error', err);
216 | }
217 | },
218 | };
219 | }
220 |
221 | function minifyShaders(config: { shouldMinify: boolean, shaderDirectory: string, output: string, debounce: number }): Plugin {
222 | function doMinification() {
223 | debounce(async () => {
224 | const filesInShaderDir = await fs.readdir(config.shaderDirectory);
225 | const shaderFiles = filesInShaderDir.filter(file => file.endsWith('glsl'));
226 | const fileArgs = shaderFiles.map(filename => {
227 | return config.shaderDirectory.endsWith('/') ? `${config.shaderDirectory}${filename}` : `${config.shaderDirectory}/${filename}`;
228 | }).join(' ');
229 | const monoRunner = process.platform === 'win32' ? '' : 'mono ';
230 | exec(`${monoRunner}shader_minifier.exe --format js ${fileArgs} -o ${config.output}`);
231 | }, config.debounce);
232 | }
233 |
234 | return {
235 | name: 'vite:shader-minify',
236 | handleHotUpdate(context: HmrContext) {
237 | if (!config.shouldMinify || !context.file.includes('glsl')) {
238 | return;
239 | }
240 |
241 | doMinification();
242 | }
243 | };
244 | }
245 |
246 | let debounceTimeout: ReturnType;
247 |
248 | export function debounce(callback: (...args: any[]) => any, wait: number) {
249 | clearTimeout(debounceTimeout);
250 | debounceTimeout = setTimeout(callback, wait);
251 | }
252 |
--------------------------------------------------------------------------------
/src/engine/svg-maker/base.ts:
--------------------------------------------------------------------------------
1 | export type LengthOrPercentage = `${number}%` | `${number}` | number;
2 | export type SvgString = `