mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
Add improved terrain compute shaders with lite and heavy variants
- Add terrain-compute-improved-lite.wgsl and terrain-compute-improved-heavy.wgsl - Create TerrainShaderRegistry.ts for shader management - Refactor TerrainComputePass to support dynamic shader switching - Update TerritoryRenderer, TerritoryLayer, and GroundTruthData for new shader integration - Enhance WebGPUDebugOverlay with additional debugging capabilities
This commit is contained in:
@@ -11,6 +11,10 @@ import {
|
||||
} from "../../InputHandler";
|
||||
import { FrameProfiler } from "../FrameProfiler";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import {
|
||||
buildTerrainShaderParams,
|
||||
readTerrainShaderId,
|
||||
} from "../webgpu/render/TerrainShaderRegistry";
|
||||
import {
|
||||
buildTerritoryPostSmoothingParams,
|
||||
readTerritoryPostSmoothingId,
|
||||
@@ -43,6 +47,7 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
private lastPaletteSignature: string | null = null;
|
||||
private lastDefensePostsSignature: string | null = null;
|
||||
private lastTerrainShaderSignature: string | null = null;
|
||||
private lastTerritoryShaderSignature: string | null = null;
|
||||
private lastPreSmoothingSignature: string | null = null;
|
||||
private lastPostSmoothingSignature: string | null = null;
|
||||
@@ -87,6 +92,7 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
this.refreshPaletteIfNeeded();
|
||||
this.refreshDefensePostsIfNeeded();
|
||||
this.applyTerrainShaderSettings();
|
||||
this.applyTerritoryShaderSettings();
|
||||
this.applyTerritorySmoothingSettings();
|
||||
|
||||
@@ -124,6 +130,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.territoryRenderer = renderer;
|
||||
this.territoryRenderer.setAlternativeView(this.alternativeView);
|
||||
this.territoryRenderer.setHighlightedOwnerId(this.hoveredOwnerSmallId);
|
||||
this.applyTerrainShaderSettings(true);
|
||||
this.applyTerritoryShaderSettings(true);
|
||||
this.applyTerritorySmoothingSettings(true);
|
||||
this.territoryRenderer.markAllDirty();
|
||||
@@ -335,6 +342,25 @@ export class TerritoryLayer implements Layer {
|
||||
this.territoryRenderer.setTerritoryShaderParams(params0, params1);
|
||||
}
|
||||
|
||||
private applyTerrainShaderSettings(force: boolean = false) {
|
||||
if (!this.territoryRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const terrainId = readTerrainShaderId(this.userSettings);
|
||||
const { shaderPath, params0, params1 } = buildTerrainShaderParams(
|
||||
this.userSettings,
|
||||
terrainId,
|
||||
);
|
||||
const signature = `${shaderPath}:${Array.from(params0).join(",")}:${Array.from(params1).join(",")}`;
|
||||
if (!force && signature === this.lastTerrainShaderSignature) {
|
||||
return;
|
||||
}
|
||||
this.lastTerrainShaderSignature = signature;
|
||||
this.territoryRenderer.setTerrainShader(shaderPath);
|
||||
this.territoryRenderer.setTerrainShaderParams(params0, params1);
|
||||
}
|
||||
|
||||
private applyTerritorySmoothingSettings(force: boolean = false) {
|
||||
if (!this.territoryRenderer) {
|
||||
return;
|
||||
|
||||
@@ -4,6 +4,13 @@ import { live } from "lit/directives/live.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { WebGPUComputeMetricsEvent } from "../../InputHandler";
|
||||
import {
|
||||
TERRAIN_SHADER_KEY,
|
||||
TERRAIN_SHADERS,
|
||||
terrainShaderIdFromInt,
|
||||
terrainShaderIntFromId,
|
||||
TerrainShaderOption,
|
||||
} from "../webgpu/render/TerrainShaderRegistry";
|
||||
import {
|
||||
TERRITORY_POST_SMOOTHING,
|
||||
TERRITORY_POST_SMOOTHING_KEY,
|
||||
@@ -21,9 +28,12 @@ import {
|
||||
TERRITORY_SHADERS,
|
||||
territoryShaderIdFromInt,
|
||||
territoryShaderIntFromId,
|
||||
TerritoryShaderOption,
|
||||
} from "../webgpu/render/TerritoryShaderRegistry";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
type ShaderOption = TerrainShaderOption | TerritoryShaderOption;
|
||||
|
||||
@customElement("webgpu-debug-overlay")
|
||||
export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
@property({ type: Object })
|
||||
@@ -186,6 +196,18 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private selectedTerrainShaderId() {
|
||||
const selected = this.userSettings.getInt(TERRAIN_SHADER_KEY, 0);
|
||||
return terrainShaderIdFromInt(selected);
|
||||
}
|
||||
|
||||
private setSelectedTerrainShaderId(
|
||||
id: "classic" | "improved-lite" | "improved-heavy",
|
||||
) {
|
||||
this.userSettings.setInt(TERRAIN_SHADER_KEY, terrainShaderIntFromId(id));
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private selectedPreSmoothingId() {
|
||||
const selected = this.userSettings.getInt(TERRITORY_PRE_SMOOTHING_KEY, 0);
|
||||
return territoryPreSmoothingIdFromInt(selected);
|
||||
@@ -212,9 +234,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private renderOptionControl(
|
||||
option: (typeof TERRITORY_SHADERS)[number]["options"][number],
|
||||
) {
|
||||
private renderOptionControl(option: ShaderOption) {
|
||||
if (option.kind === "boolean") {
|
||||
const enabled = this.userSettings.get(option.key, option.defaultValue);
|
||||
return html`
|
||||
@@ -289,6 +309,10 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
const shaderId = this.selectedShaderId();
|
||||
const shader =
|
||||
TERRITORY_SHADERS.find((s) => s.id === shaderId) ?? TERRITORY_SHADERS[0];
|
||||
const terrainShaderId = this.selectedTerrainShaderId();
|
||||
const terrainShader =
|
||||
TERRAIN_SHADERS.find((s) => s.id === terrainShaderId) ??
|
||||
TERRAIN_SHADERS[0];
|
||||
const preId = this.selectedPreSmoothingId();
|
||||
const pre =
|
||||
TERRITORY_PRE_SMOOTHING.find((s) => s.id === preId) ??
|
||||
@@ -315,6 +339,31 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sectionTitle">Shaders</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label">Terrain Shader</div>
|
||||
<select
|
||||
.value=${live(String(terrainShaderIntFromId(terrainShaderId)))}
|
||||
@change=${(e: Event) => {
|
||||
const raw = (e.target as HTMLSelectElement).value;
|
||||
const next = terrainShaderIdFromInt(Number.parseInt(raw, 10));
|
||||
this.setSelectedTerrainShaderId(next);
|
||||
}}
|
||||
>
|
||||
${TERRAIN_SHADERS.map(
|
||||
(s) =>
|
||||
html`<option value=${String(terrainShaderIntFromId(s.id))}>
|
||||
${s.label}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
${terrainShader.options.map((opt) => this.renderOptionControl(opt))}
|
||||
|
||||
<div class="sectionTitle">Territory</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label">Territory Shader</div>
|
||||
<select
|
||||
|
||||
@@ -33,6 +33,9 @@ export class TerritoryRenderer {
|
||||
private territoryShaderPath = "render/territory.wgsl";
|
||||
private territoryShaderParams0 = new Float32Array(4);
|
||||
private territoryShaderParams1 = new Float32Array(4);
|
||||
private terrainShaderPath = "compute/terrain-compute.wgsl";
|
||||
private terrainShaderParams0 = new Float32Array(4);
|
||||
private terrainShaderParams1 = new Float32Array(4);
|
||||
private preSmoothingShaderPath = "compute/visual-state-smoothing.wgsl";
|
||||
private preSmoothingParams0 = new Float32Array(4);
|
||||
private postSmoothingShaderPath = "render/temporal-resolve.wgsl";
|
||||
@@ -117,6 +120,10 @@ export class TerritoryRenderer {
|
||||
this.territoryShaderParams0,
|
||||
this.territoryShaderParams1,
|
||||
);
|
||||
this.resources.setTerrainShaderParams(
|
||||
this.terrainShaderParams0,
|
||||
this.terrainShaderParams1,
|
||||
);
|
||||
|
||||
// Upload terrain data and params (terrain colors will be computed on GPU)
|
||||
this.resources.uploadTerrainData();
|
||||
@@ -124,6 +131,7 @@ export class TerritoryRenderer {
|
||||
|
||||
// Create compute passes (terrain compute should run first)
|
||||
this.terrainComputePass = new TerrainComputePass();
|
||||
void this.terrainComputePass.setShader(this.terrainShaderPath);
|
||||
this.stateUpdatePass = new StateUpdatePass();
|
||||
this.defendedStrengthFullPass = new DefendedStrengthFullPass();
|
||||
this.defendedStrengthPass = new DefendedStrengthPass();
|
||||
@@ -278,6 +286,16 @@ export class TerritoryRenderer {
|
||||
this.resources?.invalidateHistory();
|
||||
}
|
||||
|
||||
setTerrainShader(shaderPath: string): void {
|
||||
this.terrainShaderPath = shaderPath;
|
||||
if (!this.terrainComputePass) {
|
||||
return;
|
||||
}
|
||||
void this.terrainComputePass.setShader(shaderPath).then(() => {
|
||||
this.refreshTerrain();
|
||||
});
|
||||
}
|
||||
|
||||
setTerritoryShaderParams(
|
||||
params0: Float32Array | number[],
|
||||
params1: Float32Array | number[],
|
||||
@@ -297,6 +315,25 @@ export class TerritoryRenderer {
|
||||
this.resources.invalidateHistory();
|
||||
}
|
||||
|
||||
setTerrainShaderParams(
|
||||
params0: Float32Array | number[],
|
||||
params1: Float32Array | number[],
|
||||
): void {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.terrainShaderParams0[i] = Number(params0[i] ?? 0);
|
||||
this.terrainShaderParams1[i] = Number(params1[i] ?? 0);
|
||||
}
|
||||
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.setTerrainShaderParams(
|
||||
this.terrainShaderParams0,
|
||||
this.terrainShaderParams1,
|
||||
);
|
||||
this.refreshTerrain();
|
||||
}
|
||||
|
||||
setPreSmoothing(
|
||||
enabled: boolean,
|
||||
shaderPath: string,
|
||||
|
||||
@@ -16,36 +16,28 @@ export class TerrainComputePass implements ComputePass {
|
||||
private device: GPUDevice | null = null;
|
||||
private resources: GroundTruthData | null = null;
|
||||
private needsCompute = true;
|
||||
private shaderPath = "compute/terrain-compute.wgsl";
|
||||
|
||||
async init(device: GPUDevice, resources: GroundTruthData): Promise<void> {
|
||||
this.device = device;
|
||||
this.resources = resources;
|
||||
|
||||
const shaderCode = await loadShader("compute/terrain-compute.wgsl");
|
||||
const shaderModule = device.createShaderModule({ code: shaderCode });
|
||||
this.ensureBindGroupLayout();
|
||||
await this.setShader(this.shaderPath);
|
||||
this.rebuildBindGroup();
|
||||
}
|
||||
|
||||
this.bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
texture: { sampleType: "uint" },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
storageTexture: { format: "rgba8unorm" },
|
||||
},
|
||||
],
|
||||
});
|
||||
async setShader(shaderPath: string): Promise<void> {
|
||||
this.shaderPath = shaderPath;
|
||||
if (!this.device || !this.bindGroupLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pipeline = device.createComputePipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
const shaderCode = await loadShader(shaderPath);
|
||||
const shaderModule = this.device.createShaderModule({ code: shaderCode });
|
||||
|
||||
this.pipeline = this.device.createComputePipeline({
|
||||
layout: this.device.createPipelineLayout({
|
||||
bindGroupLayouts: [this.bindGroupLayout],
|
||||
}),
|
||||
compute: {
|
||||
@@ -54,7 +46,7 @@ export class TerrainComputePass implements ComputePass {
|
||||
},
|
||||
});
|
||||
|
||||
this.rebuildBindGroup();
|
||||
this.needsCompute = true;
|
||||
}
|
||||
|
||||
needsUpdate(): boolean {
|
||||
@@ -111,6 +103,32 @@ export class TerrainComputePass implements ComputePass {
|
||||
});
|
||||
}
|
||||
|
||||
private ensureBindGroupLayout(): void {
|
||||
if (!this.device || this.bindGroupLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindGroupLayout = this.device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
texture: { sampleType: "uint" },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
storageTexture: { format: "rgba8unorm" },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
markDirty(): void {
|
||||
this.needsCompute = true;
|
||||
// Rebuild bind group in case terrain params buffer was recreated
|
||||
|
||||
@@ -86,7 +86,7 @@ export class GroundTruthData {
|
||||
// Uniform data arrays
|
||||
private readonly uniformData = new Float32Array(20);
|
||||
private readonly temporalData = new Float32Array(8);
|
||||
private readonly terrainParamsData = new Float32Array(24); // 6 vec4f: shore, water, shorelineWater, plainsBase, highlandBase, mountainBase
|
||||
private readonly terrainParamsData = new Float32Array(32); // 8 vec4f: base colors + tuning
|
||||
private readonly stateUpdateParamsData = new Uint32Array(4); // updateCount, range, pad, pad
|
||||
private readonly defendedStrengthParamsData = new Uint32Array(4); // dirtyCount, range, pad, pad
|
||||
|
||||
@@ -101,6 +101,8 @@ export class GroundTruthData {
|
||||
|
||||
private territoryShaderParams0 = new Float32Array(4);
|
||||
private territoryShaderParams1 = new Float32Array(4);
|
||||
private terrainShaderParams0 = new Float32Array([0.0, 2.5, 0.6, 0.7]);
|
||||
private terrainShaderParams1 = new Float32Array([0.0, 0.9, 0.6, 0.05]);
|
||||
|
||||
private paletteMaxSmallId = 0;
|
||||
private ownerIndexWidth = 1;
|
||||
@@ -153,9 +155,9 @@ export class GroundTruthData {
|
||||
usage: UNIFORM | COPY_DST_BUF,
|
||||
});
|
||||
|
||||
// Terrain params: 6x vec4f = 96 bytes (shore, water, shorelineWater, plainsBase, highlandBase, mountainBase)
|
||||
// Terrain params: 8x vec4f = 128 bytes (base colors + tuning)
|
||||
this.terrainParamsBuffer = device.createBuffer({
|
||||
size: 96,
|
||||
size: 128,
|
||||
usage: UNIFORM | COPY_DST_BUF,
|
||||
});
|
||||
|
||||
@@ -292,6 +294,17 @@ export class GroundTruthData {
|
||||
}
|
||||
}
|
||||
|
||||
setTerrainShaderParams(
|
||||
params0: Float32Array | number[],
|
||||
params1: Float32Array | number[],
|
||||
): void {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.terrainShaderParams0[i] = Number(params0[i] ?? 0);
|
||||
this.terrainShaderParams1[i] = Number(params1[i] ?? 0);
|
||||
}
|
||||
this.needsTerrainParamsUpload = true;
|
||||
}
|
||||
|
||||
setUseVisualStateTexture(enabled: boolean): void {
|
||||
this.useVisualStateTexture = enabled;
|
||||
if (enabled) {
|
||||
@@ -674,6 +687,16 @@ export class GroundTruthData {
|
||||
this.terrainParamsData[22] = mountainColor.b / 255;
|
||||
this.terrainParamsData[23] = 1.0;
|
||||
|
||||
// Index 24-31: tuning params (shader-dependent)
|
||||
this.terrainParamsData[24] = this.terrainShaderParams0[0];
|
||||
this.terrainParamsData[25] = this.terrainShaderParams0[1];
|
||||
this.terrainParamsData[26] = this.terrainShaderParams0[2];
|
||||
this.terrainParamsData[27] = this.terrainShaderParams0[3];
|
||||
this.terrainParamsData[28] = this.terrainShaderParams1[0];
|
||||
this.terrainParamsData[29] = this.terrainShaderParams1[1];
|
||||
this.terrainParamsData[30] = this.terrainShaderParams1[2];
|
||||
this.terrainParamsData[31] = this.terrainShaderParams1[3];
|
||||
|
||||
this.device.queue.writeBuffer(
|
||||
this.terrainParamsBuffer,
|
||||
0,
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
export type TerrainShaderId = "classic" | "improved-lite" | "improved-heavy";
|
||||
|
||||
export type TerrainShaderOption =
|
||||
| {
|
||||
kind: "boolean";
|
||||
key: string;
|
||||
label: string;
|
||||
defaultValue: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "range";
|
||||
key: string;
|
||||
label: string;
|
||||
defaultValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
| {
|
||||
kind: "enum";
|
||||
key: string;
|
||||
label: string;
|
||||
defaultValue: number;
|
||||
options: Array<{ value: number; label: string }>;
|
||||
};
|
||||
|
||||
export interface TerrainShaderDefinition {
|
||||
id: TerrainShaderId;
|
||||
label: string;
|
||||
wgslPath: string;
|
||||
options: TerrainShaderOption[];
|
||||
}
|
||||
|
||||
export const TERRAIN_SHADER_KEY = "settings.webgpu.terrain.shader";
|
||||
|
||||
export const TERRAIN_SHADERS: TerrainShaderDefinition[] = [
|
||||
{
|
||||
id: "classic",
|
||||
label: "Classic",
|
||||
wgslPath: "compute/terrain-compute.wgsl",
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: "improved-lite",
|
||||
label: "Improved (Lite)",
|
||||
wgslPath: "compute/terrain-compute-improved-lite.wgsl",
|
||||
options: [
|
||||
{
|
||||
kind: "range",
|
||||
key: "settings.webgpu.terrain.improvedLite.noiseStrength",
|
||||
label: "Noise Strength",
|
||||
defaultValue: 0.025,
|
||||
min: 0,
|
||||
max: 0.08,
|
||||
step: 0.005,
|
||||
},
|
||||
{
|
||||
kind: "range",
|
||||
key: "settings.webgpu.terrain.improvedLite.blendWidth",
|
||||
label: "Biome Blend Width",
|
||||
defaultValue: 2.5,
|
||||
min: 0.5,
|
||||
max: 5,
|
||||
step: 0.25,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "improved-heavy",
|
||||
label: "Improved (Heavy)",
|
||||
wgslPath: "compute/terrain-compute-improved-heavy.wgsl",
|
||||
options: [
|
||||
{
|
||||
kind: "range",
|
||||
key: "settings.webgpu.terrain.improvedHeavy.noiseStrength",
|
||||
label: "Noise Strength",
|
||||
defaultValue: 0.025,
|
||||
min: 0,
|
||||
max: 0.1,
|
||||
step: 0.005,
|
||||
},
|
||||
{
|
||||
kind: "range",
|
||||
key: "settings.webgpu.terrain.improvedHeavy.detailNoiseStrength",
|
||||
label: "Detail Noise Strength",
|
||||
defaultValue: 0.015,
|
||||
min: 0,
|
||||
max: 0.08,
|
||||
step: 0.005,
|
||||
},
|
||||
{
|
||||
kind: "range",
|
||||
key: "settings.webgpu.terrain.improvedHeavy.blendWidth",
|
||||
label: "Biome Blend Width",
|
||||
defaultValue: 2.8,
|
||||
min: 0.5,
|
||||
max: 6,
|
||||
step: 0.25,
|
||||
},
|
||||
{
|
||||
kind: "range",
|
||||
key: "settings.webgpu.terrain.improvedHeavy.lightingStrength",
|
||||
label: "Lighting Strength",
|
||||
defaultValue: 0.9,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
},
|
||||
{
|
||||
kind: "range",
|
||||
key: "settings.webgpu.terrain.improvedHeavy.cavityStrength",
|
||||
label: "Cavity Strength",
|
||||
defaultValue: 0.6,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function getTerrainShaderById(
|
||||
id: TerrainShaderId,
|
||||
): TerrainShaderDefinition {
|
||||
const found = TERRAIN_SHADERS.find((s) => s.id === id);
|
||||
if (!found) {
|
||||
throw new Error(`Unknown terrain shader: ${id}`);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export function terrainShaderIdFromInt(value: number): TerrainShaderId {
|
||||
if (value === 1) return "improved-lite";
|
||||
if (value === 2) return "improved-heavy";
|
||||
return "classic";
|
||||
}
|
||||
|
||||
export function terrainShaderIntFromId(id: TerrainShaderId): number {
|
||||
if (id === "improved-lite") return 1;
|
||||
if (id === "improved-heavy") return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function readTerrainShaderId(userSettings: {
|
||||
getInt: (key: string, defaultValue: number) => number;
|
||||
}): TerrainShaderId {
|
||||
return terrainShaderIdFromInt(userSettings.getInt(TERRAIN_SHADER_KEY, 0));
|
||||
}
|
||||
|
||||
export function buildTerrainShaderParams(
|
||||
userSettings: {
|
||||
getFloat: (key: string, defaultValue: number) => number;
|
||||
},
|
||||
shaderId: TerrainShaderId,
|
||||
): { shaderPath: string; params0: Float32Array; params1: Float32Array } {
|
||||
const shorelineMixLand = 0.6;
|
||||
const shorelineMixWater = 0.7;
|
||||
const specularStrength = 0.05;
|
||||
|
||||
if (shaderId === "improved-lite") {
|
||||
const noiseStrength = userSettings.getFloat(
|
||||
"settings.webgpu.terrain.improvedLite.noiseStrength",
|
||||
0.025,
|
||||
);
|
||||
const blendWidth = userSettings.getFloat(
|
||||
"settings.webgpu.terrain.improvedLite.blendWidth",
|
||||
2.5,
|
||||
);
|
||||
|
||||
const params0 = new Float32Array([
|
||||
noiseStrength,
|
||||
blendWidth,
|
||||
shorelineMixLand,
|
||||
shorelineMixWater,
|
||||
]);
|
||||
const params1 = new Float32Array([0, 0, 0, specularStrength]);
|
||||
return {
|
||||
shaderPath: "compute/terrain-compute-improved-lite.wgsl",
|
||||
params0,
|
||||
params1,
|
||||
};
|
||||
}
|
||||
|
||||
if (shaderId === "improved-heavy") {
|
||||
const noiseStrength = userSettings.getFloat(
|
||||
"settings.webgpu.terrain.improvedHeavy.noiseStrength",
|
||||
0.025,
|
||||
);
|
||||
const detailNoiseStrength = userSettings.getFloat(
|
||||
"settings.webgpu.terrain.improvedHeavy.detailNoiseStrength",
|
||||
0.015,
|
||||
);
|
||||
const blendWidth = userSettings.getFloat(
|
||||
"settings.webgpu.terrain.improvedHeavy.blendWidth",
|
||||
2.8,
|
||||
);
|
||||
const lightingStrength = userSettings.getFloat(
|
||||
"settings.webgpu.terrain.improvedHeavy.lightingStrength",
|
||||
0.9,
|
||||
);
|
||||
const cavityStrength = userSettings.getFloat(
|
||||
"settings.webgpu.terrain.improvedHeavy.cavityStrength",
|
||||
0.6,
|
||||
);
|
||||
|
||||
const params0 = new Float32Array([
|
||||
noiseStrength,
|
||||
blendWidth,
|
||||
shorelineMixLand,
|
||||
shorelineMixWater,
|
||||
]);
|
||||
const params1 = new Float32Array([
|
||||
detailNoiseStrength,
|
||||
lightingStrength,
|
||||
cavityStrength,
|
||||
specularStrength,
|
||||
]);
|
||||
return {
|
||||
shaderPath: "compute/terrain-compute-improved-heavy.wgsl",
|
||||
params0,
|
||||
params1,
|
||||
};
|
||||
}
|
||||
|
||||
const params0 = new Float32Array([
|
||||
0,
|
||||
2.5,
|
||||
shorelineMixLand,
|
||||
shorelineMixWater,
|
||||
]);
|
||||
const params1 = new Float32Array([0, 0, 0, specularStrength]);
|
||||
return { shaderPath: "compute/terrain-compute.wgsl", params0, params1 };
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
struct TerrainParams {
|
||||
shoreColor: vec4f, // Shore (land adjacent to water)
|
||||
waterColor: vec4f, // Deep water base color
|
||||
shorelineWaterColor: vec4f, // Water near shore
|
||||
plainsBaseColor: vec4f, // Plains base RGB (magnitude 0)
|
||||
highlandBaseColor: vec4f, // Highland base RGB (magnitude 10)
|
||||
mountainBaseColor: vec4f, // Mountain base RGB (magnitude 20)
|
||||
tuning0: vec4f, // x=noiseStrength, y=blendWidth, z=shoreMixLand, w=shoreMixWater
|
||||
tuning1: vec4f, // x=detailNoise, y=lightingStrength, z=cavityStrength, w=specularStrength
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: TerrainParams;
|
||||
@group(0) @binding(1) var terrainDataTex: texture_2d<u32>;
|
||||
@group(0) @binding(2) var terrainTex: texture_storage_2d<rgba8unorm, write>;
|
||||
|
||||
// Terrain bit constants (matching GameMapImpl)
|
||||
const IS_LAND_BIT: u32 = 7u;
|
||||
const SHORELINE_BIT: u32 = 6u;
|
||||
const MAGNITUDE_MASK: u32 = 0x1fu;
|
||||
|
||||
fn hash21(p: vec2u) -> f32 {
|
||||
var n = p.x * 0x9e3779b9u + p.y * 0x7f4a7c15u;
|
||||
n ^= n >> 16u;
|
||||
n *= 0x85ebca6bu;
|
||||
n ^= n >> 13u;
|
||||
n *= 0xc2b2ae35u;
|
||||
n ^= n >> 16u;
|
||||
return f32(n & 0x00ffffffu) / 16777215.0;
|
||||
}
|
||||
|
||||
fn clampCoord(coord: vec2i, dims: vec2u) -> vec2i {
|
||||
let maxX = i32(dims.x) - 1;
|
||||
let maxY = i32(dims.y) - 1;
|
||||
return vec2i(clamp(coord.x, 0, maxX), clamp(coord.y, 0, maxY));
|
||||
}
|
||||
|
||||
fn sampleMagnitude(coord: vec2i, dims: vec2u) -> f32 {
|
||||
let c = clampCoord(coord, dims);
|
||||
let data = textureLoad(terrainDataTex, c, 0).x;
|
||||
return f32(data & MAGNITUDE_MASK);
|
||||
}
|
||||
|
||||
fn computeLandColor(
|
||||
mag: f32,
|
||||
noise: f32,
|
||||
noiseStrength: f32,
|
||||
blendWidth: f32,
|
||||
) -> vec3f {
|
||||
let plainsG = max(params.plainsBaseColor.g - (2.0 * mag) / 255.0, 0.0);
|
||||
let plains = vec3f(params.plainsBaseColor.r, plainsG, params.plainsBaseColor.b);
|
||||
|
||||
let highlandMag = clamp(mag - 10.0, 0.0, 9.0);
|
||||
let highland = vec3f(
|
||||
min(params.highlandBaseColor.r + (2.0 * highlandMag) / 255.0, 1.0),
|
||||
min(params.highlandBaseColor.g + (2.0 * highlandMag) / 255.0, 1.0),
|
||||
min(params.highlandBaseColor.b + (2.0 * highlandMag) / 255.0, 1.0),
|
||||
);
|
||||
|
||||
let mountainMag = max(mag - 20.0, 0.0);
|
||||
let gray = min(params.mountainBaseColor.r + (mountainMag / 2.0) / 255.0, 1.0);
|
||||
let mountain = vec3f(gray, gray, gray);
|
||||
|
||||
let tHigh = smoothstep(10.0 - blendWidth, 10.0 + blendWidth, mag);
|
||||
let tMount = smoothstep(20.0 - blendWidth, 20.0 + blendWidth, mag);
|
||||
var land = mix(plains, highland, tHigh);
|
||||
land = mix(land, mountain, tMount);
|
||||
|
||||
let noiseBias = (noise - 0.5) * noiseStrength;
|
||||
return clamp(land + vec3f(noiseBias), vec3f(0.0), vec3f(1.0));
|
||||
}
|
||||
|
||||
fn computeWaterColor(mag: f32, noise: f32, noiseStrength: f32) -> vec3f {
|
||||
let depth = clamp(mag / 10.0, 0.0, 1.0);
|
||||
var water = mix(params.shorelineWaterColor.rgb, params.waterColor.rgb, depth);
|
||||
let noiseBias = (noise - 0.5) * noiseStrength;
|
||||
water = clamp(water + vec3f(noiseBias), vec3f(0.0), vec3f(1.0));
|
||||
return water;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(8, 8)
|
||||
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let x = i32(globalId.x);
|
||||
let y = i32(globalId.y);
|
||||
let dims = textureDimensions(terrainDataTex);
|
||||
|
||||
if (x < 0 || y < 0 || u32(x) >= dims.x || u32(y) >= dims.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let texCoord = vec2i(x, y);
|
||||
let terrainData = textureLoad(terrainDataTex, texCoord, 0).x;
|
||||
|
||||
let isLand = (terrainData & (1u << IS_LAND_BIT)) != 0u;
|
||||
let isShoreline = (terrainData & (1u << SHORELINE_BIT)) != 0u;
|
||||
let magnitude = terrainData & MAGNITUDE_MASK;
|
||||
let mag = f32(magnitude);
|
||||
|
||||
let noise = hash21(vec2u(texCoord));
|
||||
let noiseFine = hash21(vec2u(texCoord) * 3u + vec2u(17u, 29u));
|
||||
let noiseStrength = max(params.tuning0.x, 0.0);
|
||||
let blendWidth = max(params.tuning0.y, 0.1);
|
||||
let shoreMixLand = clamp(params.tuning0.z, 0.0, 1.0);
|
||||
let shoreMixWater = clamp(params.tuning0.w, 0.0, 1.0);
|
||||
let detailNoiseStrength = max(params.tuning1.x, 0.0);
|
||||
let lightingStrength = clamp(params.tuning1.y, 0.0, 1.0);
|
||||
let cavityStrength = clamp(params.tuning1.z, 0.0, 1.0);
|
||||
let specularStrength = max(params.tuning1.w, 0.0);
|
||||
|
||||
let hC = mag / 31.0;
|
||||
let hL = sampleMagnitude(texCoord + vec2i(-1, 0), dims) / 31.0;
|
||||
let hR = sampleMagnitude(texCoord + vec2i(1, 0), dims) / 31.0;
|
||||
let hD = sampleMagnitude(texCoord + vec2i(0, -1), dims) / 31.0;
|
||||
let hU = sampleMagnitude(texCoord + vec2i(0, 1), dims) / 31.0;
|
||||
|
||||
let dx = hR - hL;
|
||||
let dy = hU - hD;
|
||||
let normal = normalize(vec3f(-dx * 2.2, -dy * 2.2, 1.0));
|
||||
let lightDir = normalize(vec3f(0.55, 0.45, 1.0));
|
||||
let diffuse = clamp(dot(normal, lightDir), 0.0, 1.0);
|
||||
let baseLighting = 0.55 + 0.45 * diffuse;
|
||||
let lighting = mix(1.0, baseLighting, lightingStrength);
|
||||
|
||||
let slope = length(vec2f(dx, dy));
|
||||
let rockiness = smoothstep(0.08, 0.28, slope);
|
||||
|
||||
let cavity = clamp(((hL + hR + hD + hU) * 0.25 - hC) * 2.0, 0.0, 0.25);
|
||||
|
||||
var color: vec4f;
|
||||
|
||||
if (isLand) {
|
||||
var land = computeLandColor(mag, noise, noiseStrength, blendWidth);
|
||||
|
||||
if (isShoreline) {
|
||||
land = mix(land, params.shoreColor.rgb, shoreMixLand);
|
||||
}
|
||||
|
||||
land = mix(land, params.mountainBaseColor.rgb, rockiness * 0.6);
|
||||
|
||||
land = clamp(land * lighting, vec3f(0.0), vec3f(1.0));
|
||||
land = clamp(land * (1.0 - cavity * cavityStrength), vec3f(0.0), vec3f(1.0));
|
||||
land = clamp(
|
||||
land + vec3f((noiseFine - 0.5) * detailNoiseStrength),
|
||||
vec3f(0.0),
|
||||
vec3f(1.0),
|
||||
);
|
||||
|
||||
color = vec4f(land, 1.0);
|
||||
} else {
|
||||
var water = computeWaterColor(mag, noise, noiseStrength * 0.6);
|
||||
|
||||
if (isShoreline) {
|
||||
water = mix(water, params.shorelineWaterColor.rgb, shoreMixWater);
|
||||
}
|
||||
|
||||
let viewDir = vec3f(0.0, 0.0, 1.0);
|
||||
let spec = pow(max(dot(reflect(-lightDir, normal), viewDir), 0.0), 24.0);
|
||||
water = clamp(
|
||||
water + vec3f(spec * specularStrength),
|
||||
vec3f(0.0),
|
||||
vec3f(1.0),
|
||||
);
|
||||
|
||||
color = vec4f(water, 1.0);
|
||||
}
|
||||
|
||||
textureStore(terrainTex, texCoord, color);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
struct TerrainParams {
|
||||
shoreColor: vec4f, // Shore (land adjacent to water)
|
||||
waterColor: vec4f, // Deep water base color
|
||||
shorelineWaterColor: vec4f, // Water near shore
|
||||
plainsBaseColor: vec4f, // Plains base RGB (magnitude 0)
|
||||
highlandBaseColor: vec4f, // Highland base RGB (magnitude 10)
|
||||
mountainBaseColor: vec4f, // Mountain base RGB (magnitude 20)
|
||||
tuning0: vec4f, // x=noiseStrength, y=blendWidth, z=shoreMixLand, w=shoreMixWater
|
||||
tuning1: vec4f, // unused in lite
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: TerrainParams;
|
||||
@group(0) @binding(1) var terrainDataTex: texture_2d<u32>;
|
||||
@group(0) @binding(2) var terrainTex: texture_storage_2d<rgba8unorm, write>;
|
||||
|
||||
// Terrain bit constants (matching GameMapImpl)
|
||||
const IS_LAND_BIT: u32 = 7u;
|
||||
const SHORELINE_BIT: u32 = 6u;
|
||||
const MAGNITUDE_MASK: u32 = 0x1fu;
|
||||
|
||||
fn hash21(p: vec2u) -> f32 {
|
||||
var n = p.x * 0x9e3779b9u + p.y * 0x7f4a7c15u;
|
||||
n ^= n >> 16u;
|
||||
n *= 0x85ebca6bu;
|
||||
n ^= n >> 13u;
|
||||
n *= 0xc2b2ae35u;
|
||||
n ^= n >> 16u;
|
||||
return f32(n & 0x00ffffffu) / 16777215.0;
|
||||
}
|
||||
|
||||
fn computeLandColor(mag: f32, noise: f32, noiseStrength: f32, blendWidth: f32) -> vec3f {
|
||||
let plainsG = max(params.plainsBaseColor.g - (2.0 * mag) / 255.0, 0.0);
|
||||
let plains = vec3f(params.plainsBaseColor.r, plainsG, params.plainsBaseColor.b);
|
||||
|
||||
let highlandMag = clamp(mag - 10.0, 0.0, 9.0);
|
||||
let highland = vec3f(
|
||||
min(params.highlandBaseColor.r + (2.0 * highlandMag) / 255.0, 1.0),
|
||||
min(params.highlandBaseColor.g + (2.0 * highlandMag) / 255.0, 1.0),
|
||||
min(params.highlandBaseColor.b + (2.0 * highlandMag) / 255.0, 1.0),
|
||||
);
|
||||
|
||||
let mountainMag = max(mag - 20.0, 0.0);
|
||||
let gray = min(params.mountainBaseColor.r + (mountainMag / 2.0) / 255.0, 1.0);
|
||||
let mountain = vec3f(gray, gray, gray);
|
||||
|
||||
let tHigh = smoothstep(10.0 - blendWidth, 10.0 + blendWidth, mag);
|
||||
let tMount = smoothstep(20.0 - blendWidth, 20.0 + blendWidth, mag);
|
||||
var land = mix(plains, highland, tHigh);
|
||||
land = mix(land, mountain, tMount);
|
||||
|
||||
let noiseBias = (noise - 0.5) * noiseStrength;
|
||||
return clamp(land + vec3f(noiseBias), vec3f(0.0), vec3f(1.0));
|
||||
}
|
||||
|
||||
fn computeWaterColor(mag: f32, noise: f32, noiseStrength: f32) -> vec3f {
|
||||
let depth = clamp(mag / 10.0, 0.0, 1.0);
|
||||
var water = mix(params.shorelineWaterColor.rgb, params.waterColor.rgb, depth);
|
||||
let noiseBias = (noise - 0.5) * noiseStrength;
|
||||
water = clamp(water + vec3f(noiseBias), vec3f(0.0), vec3f(1.0));
|
||||
return water;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(8, 8)
|
||||
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let x = i32(globalId.x);
|
||||
let y = i32(globalId.y);
|
||||
let dims = textureDimensions(terrainDataTex);
|
||||
|
||||
if (x < 0 || y < 0 || u32(x) >= dims.x || u32(y) >= dims.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let texCoord = vec2i(x, y);
|
||||
let terrainData = textureLoad(terrainDataTex, texCoord, 0).x;
|
||||
|
||||
let isLand = (terrainData & (1u << IS_LAND_BIT)) != 0u;
|
||||
let isShoreline = (terrainData & (1u << SHORELINE_BIT)) != 0u;
|
||||
let magnitude = terrainData & MAGNITUDE_MASK;
|
||||
let mag = f32(magnitude);
|
||||
|
||||
let noise = hash21(vec2u(texCoord));
|
||||
let noiseStrength = max(params.tuning0.x, 0.0);
|
||||
let blendWidth = max(params.tuning0.y, 0.1);
|
||||
let shoreMixLand = clamp(params.tuning0.z, 0.0, 1.0);
|
||||
let shoreMixWater = clamp(params.tuning0.w, 0.0, 1.0);
|
||||
var color: vec4f;
|
||||
|
||||
if (isLand) {
|
||||
var land = computeLandColor(mag, noise, noiseStrength, blendWidth);
|
||||
if (isShoreline) {
|
||||
land = mix(land, params.shoreColor.rgb, shoreMixLand);
|
||||
}
|
||||
color = vec4f(land, 1.0);
|
||||
} else {
|
||||
var water = computeWaterColor(mag, noise, noiseStrength * 0.6);
|
||||
if (isShoreline) {
|
||||
water = mix(water, params.shorelineWaterColor.rgb, shoreMixWater);
|
||||
}
|
||||
color = vec4f(water, 1.0);
|
||||
}
|
||||
|
||||
textureStore(terrainTex, texCoord, color);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ struct TerrainParams {
|
||||
plainsBaseColor: vec4f, // Plains base RGB (magnitude 0)
|
||||
highlandBaseColor: vec4f, // Highland base RGB (magnitude 10)
|
||||
mountainBaseColor: vec4f, // Mountain base RGB (magnitude 20)
|
||||
tuning0: vec4f, // Shader tuning params (unused in classic)
|
||||
tuning1: vec4f, // Shader tuning params (unused in classic)
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: TerrainParams;
|
||||
|
||||
Reference in New Issue
Block a user