Skip to content

Commit

Permalink
mix soldier shading with terrain shading
Browse files Browse the repository at this point in the history
  • Loading branch information
gtanczyk committed Oct 14, 2024
1 parent 9b96d5f commit 02a3118
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 52 deletions.
7 changes: 7 additions & 0 deletions games/masterplan/src/screens/battle/game/game-render-queue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Canvas } from '../util/canvas';
import { GameWorldRender } from './game-world-render';

// Define types for render commands
type RenderCommand = {
Expand All @@ -20,8 +21,14 @@ const layerOrder: { [key: string]: number } = {
};

export class RenderQueue {
constructor(private worldRender: GameWorldRender) {}

private commands: RenderCommand[] = [];

getShadingAt(x: number, y: number): number {
return this.worldRender.getShadingAt(x, y);
}

renderShape(layer: RenderCommand['layer'], x: number, y: number, z: number, points: number[][], fillStyle: string) {
this.commands.push({
layer,
Expand Down
2 changes: 1 addition & 1 deletion games/masterplan/src/screens/battle/game/game-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GameWorldRender } from './game-world-render';

export function renderGame(world: GameWorld, worldRender: GameWorldRender) {
const canvas = getCanvas(LAYER_DEFAULT);
const renderQueue = new RenderQueue();
const renderQueue = new RenderQueue(worldRender);

// clear
canvas.clear();
Expand Down
18 changes: 16 additions & 2 deletions games/masterplan/src/screens/battle/game/game-world-render.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { GameWorld } from './game-world';
import { interpolateGridValue } from './terrain/terrain';
import { createTerrainTexture } from './terrain/terrain-renderer';

export class GameWorldRender {
terrainCanvas: HTMLCanvasElement;
shadingMap: number[][];

constructor(gameWorld: GameWorld) {
this.terrainCanvas = createTerrainTexture(
constructor(private gameWorld: GameWorld) {
const { terrainCanvas, shadingMap } = createTerrainTexture(
gameWorld.terrain.width,
gameWorld.terrain.height,
gameWorld.terrain.heightMap,
gameWorld.terrain.tileSize,
);
this.terrainCanvas = terrainCanvas;
this.shadingMap = shadingMap;
}

getShadingAt(x: number, y: number): number {
return interpolateGridValue(
[x, y],
this.shadingMap,
this.gameWorld.terrain.tileSize,
this.gameWorld.terrain.offsetX,
this.gameWorld.terrain.offsetY,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,19 @@ export class SoldierRender {
// Sort shapes by z-index for proper layering
const shapes = isAlive ? SHAPES[this.soldier.type] : FALLEN_SHAPES[this.soldier.type];

// Get terrain shading factor
const terrainShadingFactor = renderQueue.getShadingAt(...this.soldier.vec);

// Render each part of the soldier
for (const [, shape] of shapes) {
const rotatedPoints = rotate3D(shape.points, rotationAngle + Math.PI / 2);
const perspectivePoints = rotatedPoints.map((p) => applyPerspective(p));
const shadedColor = applyShading(baseColor, rotationAngle, this.soldier.state.isAlive() ? 1 : 0.5);

// Apply shading to both live and fallen soldiers
const shadedColor = applyShading(baseColor, rotationAngle, isAlive ? 1 : 0.5);

// Combine soldier shading with terrain shading
const finalColor = this.combineColors(shadedColor, terrainShadingFactor);

renderQueue.addObjectCommand(
this.soldier.getX(),
Expand All @@ -68,9 +76,30 @@ export class SoldierRender {
this.soldier.world.terrain.getHeightAt(this.soldier.vec),
this.soldier.getZ(),
isAlive,
shadedColor,
finalColor,
perspectivePoints.map((p) => [p.x, p.y]),
);
}
}

private combineColors(soldierColor: string, terrainShadingFactor: number): string {
// Parse the soldier color
const rgbaMatch = soldierColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)/);
if (!rgbaMatch) {
return soldierColor;
}

const r = parseInt(rgbaMatch[1], 10);
const g = parseInt(rgbaMatch[2], 10);
const b = parseInt(rgbaMatch[3], 10);
const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1;

// Apply terrain shading factor
const newR = Math.floor(r * terrainShadingFactor);
const newG = Math.floor(g * terrainShadingFactor);
const newB = Math.floor(b * terrainShadingFactor);

// Return the combined color
return `rgba(${newR},${newG},${newB},${a})`;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EDGE_RADIUS } from '../../consts';

export type TerrainData = { width: number; height: number; heightMap: number[][] };
export type TerrainData = { width: number; height: number; tileSize: number; heightMap: number[][] };

// Helper function to get the number of active neighbors for a cell
function getActiveNeighbors(grid: number[][], x: number, y: number): number {
Expand Down Expand Up @@ -107,5 +107,5 @@ export function generateTerrain(tileSize: number): TerrainData {
const height = EDGE_RADIUS * 2;
const heightMap = generateTerrainHeightMap(width, height, tileSize);

return { width, height, heightMap };
return { width, height, tileSize, heightMap };
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,25 @@ function calculateNormal(heightMap: number[][], x: number, y: number, tileSize:
return [normal[0] / length, normal[1] / length, normal[2] / length];
}

// Function to render terrain based on heightmap
// Function to render terrain based on heightmap and generate shading map
export function renderTerrain(
ctx: CanvasRenderingContext2D,
heightMap: number[][],
width: number,
height: number,
tileSize: number,
): void {
): { terrainCanvas: HTMLCanvasElement; shadingMap: number[][] } {
const gridWidth = Math.ceil(width / tileSize);
const gridHeight = Math.ceil(height / tileSize);

const terrainCanvas = document.createElement('canvas');
terrainCanvas.width = width;
terrainCanvas.height = height;
const terrainCtx = terrainCanvas.getContext('2d')!;

const shadingMap: number[][] = Array(gridHeight)
.fill(0)
.map(() => Array(gridWidth).fill(0));

// Draw the terrain based on the heightmap
for (let y = 0; y < gridHeight; y++) {
for (let x = 0; x < gridWidth; x++) {
Expand Down Expand Up @@ -76,18 +84,24 @@ export function renderTerrain(
const shadedColor = applyShading(baseColor, rotationAngle, averageHeight);

// Draw the tile using a path to represent height variations
ctx.beginPath();
ctx.moveTo(x * tileSize, topLeftY);
ctx.lineTo((x + 1) * tileSize, topRightY);
ctx.lineTo((x + 1) * tileSize, bottomRightY);
ctx.lineTo(x * tileSize, bottomLeftY);
ctx.closePath();
terrainCtx.beginPath();
terrainCtx.moveTo(x * tileSize, topLeftY);
terrainCtx.lineTo((x + 1) * tileSize, topRightY);
terrainCtx.lineTo((x + 1) * tileSize, bottomRightY);
terrainCtx.lineTo(x * tileSize, bottomLeftY);
terrainCtx.closePath();

// Set the fill color with shading
ctx.fillStyle = shadedColor;
ctx.fill();
terrainCtx.fillStyle = shadedColor;
terrainCtx.fill();

// Calculate shading factor for the shading map
const shadingFactor = (Math.cos(rotationAngle) * 0.3 + 0.7) * (0.8 + factor * 0.4);
shadingMap[y][x] = shadingFactor;
}
}

return { terrainCanvas, shadingMap };
}

// Function to create a terrain texture
Expand All @@ -96,15 +110,8 @@ export function createTerrainTexture(
height: number,
heightMap: number[][],
tileSize: number,
): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;

renderTerrain(ctx, heightMap, width, height, tileSize);

return canvas;
): { terrainCanvas: HTMLCanvasElement; shadingMap: number[][] } {
return renderTerrain(heightMap, width, height, tileSize);
}

const interpolateColor = (color1: string, color2: string, factor: number): string => {
Expand Down
56 changes: 31 additions & 25 deletions games/masterplan/src/screens/battle/game/terrain/terrain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,36 @@ export class Terrain {
}

getHeightAt(pos: Vec): number {
// Calculate tile indices and positions within the tile
const x = pos[0] / this.tileSize + this.offsetX;
const y = pos[1] / this.tileSize + this.offsetY;

const x0 = Math.min(Math.max(Math.floor(x), 0), this.heightMap.length - 1);
const x1 = Math.min(x0 + 1, this.heightMap[0].length - 1);
const y0 = Math.min(Math.max(Math.floor(y), 0), this.heightMap.length - 1);
const y1 = Math.max(Math.min(y0 + 1, this.heightMap.length - 1), 0);

// Get heights at the four corners
const h00 = this.heightMap[y0][x0];
const h10 = this.heightMap[y0][x1];
const h01 = this.heightMap[y1][x0];
const h11 = this.heightMap[y1][x1];

// Calculate the fractional part of x and y within the tile
const dx = x - x0;
const dy = y - y0;

// Bilinear interpolation
const h0 = h00 * (1 - dx) + h10 * dx;
const h1 = h01 * (1 - dx) + h11 * dx;
const height = h0 * (1 - dy) + h1 * dy;

return height;
return interpolateGridValue(pos, this.heightMap, this.tileSize, this.offsetX, this.offsetY);
}
}

export function interpolateGridValue(
pos: Vec,
grid: number[][],
tileSize: number,
offsetX: number,
offsetY: number,
): number {
// Calculate tile indices and positions within the tile
const x = pos[0] / tileSize + offsetX;
const y = pos[1] / tileSize + offsetY;

const x0 = Math.min(Math.max(Math.floor(x), 0), grid[0].length - 1);
const x1 = Math.min(x0 + 1, grid[0].length - 1);
const y0 = Math.min(Math.max(Math.floor(y), 0), grid.length - 1);
const y1 = Math.max(Math.min(y0 + 1, grid.length - 1), 0);

const h00 = grid[y0][x0];
const h10 = grid[y0][x1];
const h01 = grid[y1][x0];
const h11 = grid[y1][x1];

const dx = x - x0;
const dy = y - y0;

const h0 = h00 * (1 - dx) + h10 * dx;
const h1 = h01 * (1 - dx) + h11 * dx;

return h0 * (1 - dy) + h1 * dy;
}
Loading

0 comments on commit 02a3118

Please sign in to comment.