Compare commits
3 Commits
d393ca98ec
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
6eb28ffcac
|
|||
|
b8917272f7
|
|||
|
d63d742695
|
@@ -75,6 +75,22 @@ Game data directory selection/persistence is managed by app managers and Flutter
|
|||||||
- **No game data discovered**: choose a valid directory in the picker and ensure files are present.
|
- **No game data discovered**: choose a valid directory in the picker and ensure files are present.
|
||||||
- **Linux build/runtime issues**: verify native dependency packages are installed.
|
- **Linux build/runtime issues**: verify native dependency packages are installed.
|
||||||
- **Web target limitations**: use desktop target for full native-audio/path behavior.
|
- **Web target limitations**: use desktop target for full native-audio/path behavior.
|
||||||
|
- **Web release build won’t load when opening `index.html` directly**: Flutter web output must be served over `http://` or `https://`, not opened with `file://`.
|
||||||
|
|
||||||
|
Build and serve locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter build web --release
|
||||||
|
python3 -m http.server 8080 -d build/web
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:8080` in your browser.
|
||||||
|
|
||||||
|
If deploying under a subpath, build with matching base href, for example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter build web --release --base-href /wolf_dart/
|
||||||
|
```
|
||||||
|
|
||||||
## Related Modules
|
## Related Modules
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
|
|
||||||
|
import 'packaged_games_loader.dart';
|
||||||
import 'wolf3d_gui_app.dart';
|
import 'wolf3d_gui_app.dart';
|
||||||
|
|
||||||
/// Creates the application shell after loading available Wolf3D data sets.
|
/// Creates the application shell after loading available Wolf3D data sets.
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
final seededGames = await loadPackagedGames();
|
||||||
|
|
||||||
final Wolf3dFlutterEngine wolf3d = await Wolf3dFlutterEngine(
|
final Wolf3dFlutterEngine wolf3d = await Wolf3dFlutterEngine(
|
||||||
debug: kDebugMode,
|
debug: kDebugMode,
|
||||||
).init();
|
).init(seededGames: seededGames);
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
wolf3d.enableMenuHeaderBandDebugLogging(prefix: '[wolf_3d_gui]');
|
wolf3d.enableMenuHeaderBandDebugLogging(prefix: '[wolf_3d_gui]');
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
|
|
||||||
|
typedef PackagedGameDataLoader =
|
||||||
|
Future<WolfensteinData> Function({
|
||||||
|
required GameVersion version,
|
||||||
|
required String assetDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<List<WolfensteinData>> loadPackagedGames({
|
||||||
|
PackagedGameDataLoader? loader,
|
||||||
|
}) async {
|
||||||
|
final PackagedGameDataLoader effectiveLoader =
|
||||||
|
loader ??
|
||||||
|
({required version, required assetDirectory}) =>
|
||||||
|
Wolf3dFlutterEngine.loadGameDataFromAssets(
|
||||||
|
version: version,
|
||||||
|
packageName: 'wolf_3d_assets',
|
||||||
|
assetDirectory: assetDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<WolfensteinData> games = [];
|
||||||
|
|
||||||
|
Future<void> tryLoad({
|
||||||
|
required GameVersion version,
|
||||||
|
required String assetDirectory,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
games.add(
|
||||||
|
await effectiveLoader(
|
||||||
|
version: version,
|
||||||
|
assetDirectory: assetDirectory,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
'Packaged game load skipped for ${version.label} ($assetDirectory): $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tryLoad(version: GameVersion.retail, assetDirectory: 'assets/retail');
|
||||||
|
await tryLoad(
|
||||||
|
version: GameVersion.shareware,
|
||||||
|
assetDirectory: 'assets/shareware',
|
||||||
|
);
|
||||||
|
await tryLoad(
|
||||||
|
version: GameVersion.spearOfDestinyDemo,
|
||||||
|
assetDirectory: 'assets/sod/shareware',
|
||||||
|
);
|
||||||
|
|
||||||
|
return games;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ dependencies:
|
|||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
wolf_3d_flutter:
|
wolf_3d_flutter:
|
||||||
wolf_3d_dart:
|
wolf_3d_dart:
|
||||||
|
wolf_3d_assets:
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class _RecordingEngine extends Wolf3dFlutterEngine {
|
|||||||
Future<Wolf3dFlutterEngine> init({
|
Future<Wolf3dFlutterEngine> init({
|
||||||
String? directory,
|
String? directory,
|
||||||
Iterable<String>? additionalDirectories,
|
Iterable<String>? additionalDirectories,
|
||||||
|
Iterable<WolfensteinData>? seededGames,
|
||||||
}) async {
|
}) async {
|
||||||
initCallCount++;
|
initCallCount++;
|
||||||
lastDirectory = directory;
|
lastDirectory = directory;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_gui/packaged_games_loader.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('loadPackagedGames', () {
|
||||||
|
test('loads known packaged directories in order', () async {
|
||||||
|
final requested = <(GameVersion, String)>[];
|
||||||
|
|
||||||
|
final games = await loadPackagedGames(
|
||||||
|
loader: ({required version, required assetDirectory}) async {
|
||||||
|
requested.add((version, assetDirectory));
|
||||||
|
return _buildTestData(version);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(requested, <(GameVersion, String)>[
|
||||||
|
(GameVersion.retail, 'assets/retail'),
|
||||||
|
(GameVersion.shareware, 'assets/shareware'),
|
||||||
|
(GameVersion.spearOfDestinyDemo, 'assets/sod/shareware'),
|
||||||
|
]);
|
||||||
|
expect(games.map((g) => g.version), <GameVersion>[
|
||||||
|
GameVersion.retail,
|
||||||
|
GameVersion.shareware,
|
||||||
|
GameVersion.spearOfDestinyDemo,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips failing packaged loads and keeps successful ones', () async {
|
||||||
|
final games = await loadPackagedGames(
|
||||||
|
loader: ({required version, required assetDirectory}) async {
|
||||||
|
if (version == GameVersion.shareware) {
|
||||||
|
throw Exception('shareware unavailable');
|
||||||
|
}
|
||||||
|
return _buildTestData(version);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(games.map((g) => g.version), <GameVersion>[
|
||||||
|
GameVersion.retail,
|
||||||
|
GameVersion.spearOfDestinyDemo,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfensteinData _buildTestData(GameVersion version) {
|
||||||
|
final wallGrid = List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
final objectGrid = List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
wallGrid[0][i] = 2;
|
||||||
|
wallGrid[63][i] = 2;
|
||||||
|
wallGrid[i][0] = 2;
|
||||||
|
wallGrid[i][63] = 2;
|
||||||
|
}
|
||||||
|
objectGrid[2][2] = MapObject.playerEast;
|
||||||
|
|
||||||
|
return WolfensteinData(
|
||||||
|
version: version,
|
||||||
|
dataVersion: DataVersion.unknown,
|
||||||
|
registry: version == GameVersion.shareware
|
||||||
|
? SharewareAssetRegistry()
|
||||||
|
: RetailAssetRegistry(),
|
||||||
|
walls: <Sprite>[_sprite(1), _sprite(1), _sprite(2), _sprite(2)],
|
||||||
|
sprites: List<Sprite>.generate(436, (_) => _sprite(255)),
|
||||||
|
sounds: List<PcmSound>.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||||
|
adLibSounds: const <PcmSound>[],
|
||||||
|
music: const <ImfMusic>[],
|
||||||
|
vgaImages: const <VgaImage>[],
|
||||||
|
episodes: <Episode>[
|
||||||
|
Episode(
|
||||||
|
name: 'Test Episode',
|
||||||
|
levels: <WolfLevel>[
|
||||||
|
WolfLevel(
|
||||||
|
name: 'Test Level',
|
||||||
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
|
objectGrid: objectGrid,
|
||||||
|
music: Music.level01,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite _sprite(int color) =>
|
||||||
|
Sprite(Uint8List.fromList(List<int>.filled(64 * 64, color)));
|
||||||
@@ -88,6 +88,7 @@ class _RecordingEngine extends Wolf3dFlutterEngine {
|
|||||||
Future<Wolf3dFlutterEngine> init({
|
Future<Wolf3dFlutterEngine> init({
|
||||||
String? directory,
|
String? directory,
|
||||||
Iterable<String>? additionalDirectories,
|
Iterable<String>? additionalDirectories,
|
||||||
|
Iterable<WolfensteinData>? seededGames,
|
||||||
}) async {
|
}) async {
|
||||||
initCallCount++;
|
initCallCount++;
|
||||||
availableGames
|
availableGames
|
||||||
|
|||||||
@@ -39,7 +39,30 @@ final Wolf3dFlutterEngine engine = await Wolf3dFlutterEngine(
|
|||||||
).init();
|
).init();
|
||||||
```
|
```
|
||||||
|
|
||||||
`init()` handles platform setup, audio init, and configured game-data discovery.
|
`init()` handles platform setup, audio init, configured external discovery, and
|
||||||
|
optional seeded game injection.
|
||||||
|
|
||||||
|
To load packaged game data from `wolf_3d_assets`, use
|
||||||
|
`Wolf3dFlutterEngine.loadGameDataFromAssets(...)` and pass the result via
|
||||||
|
`seededGames`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final retail = await Wolf3dFlutterEngine.loadGameDataFromAssets(
|
||||||
|
version: GameVersion.retail,
|
||||||
|
packageName: 'wolf_3d_assets',
|
||||||
|
assetDirectory: 'assets/retail',
|
||||||
|
);
|
||||||
|
|
||||||
|
final shareware = await Wolf3dFlutterEngine.loadGameDataFromAssets(
|
||||||
|
version: GameVersion.shareware,
|
||||||
|
packageName: 'wolf_3d_assets',
|
||||||
|
assetDirectory: 'assets/shareware',
|
||||||
|
);
|
||||||
|
|
||||||
|
final Wolf3dFlutterEngine engine = await Wolf3dFlutterEngine(
|
||||||
|
debug: kDebugMode,
|
||||||
|
).init(seededGames: [retail, shareware]);
|
||||||
|
```
|
||||||
|
|
||||||
The facade itself lives in `lib/engine/wolf3d_flutter_engine.dart` and is re-exported
|
The facade itself lives in `lib/engine/wolf3d_flutter_engine.dart` and is re-exported
|
||||||
through the package barrel at `lib/wolf_3d_flutter.dart`. External consumers
|
through the package barrel at `lib/wolf_3d_flutter.dart`. External consumers
|
||||||
@@ -63,22 +86,147 @@ For full host wiring examples, see:
|
|||||||
- `lib/renderer/` — renderer host widgets.
|
- `lib/renderer/` — renderer host widgets.
|
||||||
- `lib/managers/` — runtime/session/display/persistence managers.
|
- `lib/managers/` — runtime/session/display/persistence managers.
|
||||||
- `lib/audio/` — platform-aware audio backends.
|
- `lib/audio/` — platform-aware audio backends.
|
||||||
- `shaders/wolf_world.frag` — fragment shader included in package configuration.
|
- `shaders/wolf_world.frag` — base fragment shader included in package configuration.
|
||||||
|
- `shaders/wolf_world_bloom.frag` — bloom-enabled fragment shader variant.
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
From this directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
flutter analyze
|
|
||||||
flutter test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Notes
|
## Integration Notes
|
||||||
|
|
||||||
- Keep UI/platform concerns in this package or app hosts, not in `wolf_3d_dart`.
|
- Keep UI/platform concerns in this package or app hosts, not in `wolf_3d_dart`.
|
||||||
- Use exported APIs from `lib/wolf_3d_flutter.dart` rather than importing private internals from `lib/src` in dependencies.
|
- Use exported APIs from `lib/wolf_3d_flutter.dart` rather than importing private internals from `lib/src` in dependencies.
|
||||||
- Shader path is declared in this package `pubspec.yaml` and must stay synchronized with renderer usage.
|
- Shader paths are declared in this package `pubspec.yaml` and must stay synchronized with renderer usage.
|
||||||
|
|
||||||
|
## Shader Architecture And Performance Notes
|
||||||
|
|
||||||
|
This package ships two shader variants:
|
||||||
|
|
||||||
|
- `shaders/wolf_world.frag` (base pass, no bloom taps)
|
||||||
|
- `shaders/wolf_world_bloom.frag` (bloom-enabled variant)
|
||||||
|
|
||||||
|
The renderer selects one variant in Dart based on runtime settings. This is a
|
||||||
|
performance decision: when bloom is disabled, we do not run bloom sampling code
|
||||||
|
at all.
|
||||||
|
|
||||||
|
### No-Branch Shader Policy
|
||||||
|
|
||||||
|
For package-owned shader sources, do not use `if` statements. Use branchless
|
||||||
|
selection patterns (`mix`, `step`, `smoothstep`, mask algebra) instead.
|
||||||
|
|
||||||
|
Static check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg "\bif\s*\(" packages/wolf_3d_flutter/shaders
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: no matches in source shader files.
|
||||||
|
|
||||||
|
### Why This Is Different From Dart
|
||||||
|
|
||||||
|
If you are primarily a Dart developer, this is the key mindset shift:
|
||||||
|
|
||||||
|
- Dart code runs on CPU cores with branch prediction and comparatively cheap
|
||||||
|
control flow.
|
||||||
|
- Fragment shaders run across many pixels in parallel on GPU SIMD/SIMT lanes.
|
||||||
|
- If neighboring pixels take different branches, the GPU can serialize branch
|
||||||
|
paths (divergence), reducing throughput.
|
||||||
|
- Texture reads are usually more expensive than scalar ALU math. Removing bloom
|
||||||
|
work entirely when disabled is often better than trying to gate it inside one
|
||||||
|
shader.
|
||||||
|
|
||||||
|
In short: in Dart, `if` can be good structure. In fragment shaders, branchless
|
||||||
|
math and pass selection are often better for frame time.
|
||||||
|
|
||||||
|
### Dart-Style Thinking vs Shader-Style Thinking
|
||||||
|
|
||||||
|
CPU/Dart style:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
if (effectsEnabled) {
|
||||||
|
uv = warp(uv);
|
||||||
|
}
|
||||||
|
if (outsideScreen(uv)) {
|
||||||
|
return bezelColor;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Shader style used here:
|
||||||
|
|
||||||
|
```glsl
|
||||||
|
vec2 effectiveUv = mix(uv, warpedUv, effectsMask);
|
||||||
|
float bezelMask = (1.0 - insideMask) * effectsMask;
|
||||||
|
vec3 outColor = mix(screenColor, bezelColor, bezelMask);
|
||||||
|
```
|
||||||
|
|
||||||
|
Both produce feature-equivalent behavior, but the second keeps execution paths
|
||||||
|
uniform across fragments.
|
||||||
|
|
||||||
|
### Shader Block Guide (What / Why)
|
||||||
|
|
||||||
|
1. UV normalization:
|
||||||
|
Convert fragment coordinates to 0..1 UV so all sampling math is resolution
|
||||||
|
agnostic.
|
||||||
|
|
||||||
|
2. Barrel warp:
|
||||||
|
Simulates curved CRT glass by pushing UVs outward as radius increases.
|
||||||
|
|
||||||
|
3. Inside/outside mask:
|
||||||
|
Computes whether warped UV remains on the emissive screen rectangle. This
|
||||||
|
replaces branch-based bezel routing.
|
||||||
|
|
||||||
|
4. Edge-aware AA:
|
||||||
|
Computes local luma span from N/S/E/W neighbors and blends toward neighborhood
|
||||||
|
average only where contrast indicates potential aliasing.
|
||||||
|
|
||||||
|
5. CRT modulation:
|
||||||
|
Applies scanlines, moving sweep, center lift, and vignette to mimic phosphor
|
||||||
|
and lens behavior.
|
||||||
|
|
||||||
|
6. Bezel shading:
|
||||||
|
Uses overflow distance and edge bleed sampling to build depth, inner lip, and
|
||||||
|
scene-tinted glow on bezel regions.
|
||||||
|
|
||||||
|
7. Bloom variant only:
|
||||||
|
Adds three-ring cross taps, brightness gating, and tone mapping. This code is
|
||||||
|
in a separate shader so bloom-off mode avoids paying this cost.
|
||||||
|
|
||||||
|
### Constant Tuning Reference
|
||||||
|
|
||||||
|
- Warp factor: `0.045`
|
||||||
|
Higher = stronger curvature.
|
||||||
|
|
||||||
|
- AA blend ceiling: `0.45`
|
||||||
|
Higher = softer edges, more blur risk.
|
||||||
|
|
||||||
|
- Scanline band: `0.88 + 0.12 * sin(...)`
|
||||||
|
Lower floor or higher amplitude increases CRT stripe intensity.
|
||||||
|
|
||||||
|
- Sweep speed: `uTime * 0.08`
|
||||||
|
Higher = faster sweep line travel.
|
||||||
|
|
||||||
|
- Bloom ring radii: `3`, `7`, `13` texels
|
||||||
|
Larger radii spread glow farther but increase halo size.
|
||||||
|
|
||||||
|
- Bloom gain: `0.42`
|
||||||
|
Higher = brighter bloom before tone map.
|
||||||
|
|
||||||
|
- Tone map: `color / (color + 0.75) * 1.75`
|
||||||
|
Controls highlight rolloff and midtone lift.
|
||||||
|
|
||||||
|
### Glossary
|
||||||
|
|
||||||
|
- UV: normalized texture coordinates in [0, 1].
|
||||||
|
- Luma: perceived brightness estimate from RGB.
|
||||||
|
- Mask: scalar 0..1 value used to blend between alternatives.
|
||||||
|
- Vignette: edge darkening effect.
|
||||||
|
- Tone map: compresses highlights into displayable range.
|
||||||
|
- Tap: one texture sample read.
|
||||||
|
- Divergence: parallel shader lanes taking different branches.
|
||||||
|
|
||||||
|
### Profiling Expectations
|
||||||
|
|
||||||
|
- Bloom disabled: base shader variant runs, no bloom taps.
|
||||||
|
- Bloom enabled: bloom shader variant runs, additional texture sampling cost.
|
||||||
|
- Effects disabled: both shaders still remain branchless; effect contribution is
|
||||||
|
blended out by mask values.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
@@ -11,6 +12,8 @@ import 'package:wolf_3d_flutter/managers/desktop_windowing_support.dart'
|
|||||||
as desktop_windowing_support;
|
as desktop_windowing_support;
|
||||||
import 'package:wolf_3d_flutter/managers/game_data_directory_persistence.dart';
|
import 'package:wolf_3d_flutter/managers/game_data_directory_persistence.dart';
|
||||||
|
|
||||||
|
typedef Wolf3dAssetByteLoader = Future<ByteData> Function(String assetKey);
|
||||||
|
|
||||||
/// Flutter-specific host facade built on top of [Wolf3dEngine].
|
/// Flutter-specific host facade built on top of [Wolf3dEngine].
|
||||||
///
|
///
|
||||||
/// This type keeps platform-neutral session and engine state in
|
/// This type keeps platform-neutral session and engine state in
|
||||||
@@ -51,12 +54,105 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Routes shared menu header band diagnostics to [logger].
|
||||||
|
///
|
||||||
|
/// Pass `null` to disable menu header band diagnostics.
|
||||||
|
@override
|
||||||
|
Wolf3dFlutterEngine setMenuHeaderBandDebugLogger(
|
||||||
|
void Function(String message)? logger,
|
||||||
|
) {
|
||||||
|
super.setMenuHeaderBandDebugLogger(logger);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables menu header band diagnostics with an optional [prefix].
|
||||||
|
@override
|
||||||
|
Wolf3dFlutterEngine enableMenuHeaderBandDebugLogging({
|
||||||
|
String prefix = '[MENU_HEADER_BAND]',
|
||||||
|
}) {
|
||||||
|
super.enableMenuHeaderBandDebugLogging(prefix: prefix);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables menu header band diagnostics.
|
||||||
|
@override
|
||||||
|
Wolf3dFlutterEngine disableMenuHeaderBandDebugLogging() {
|
||||||
|
super.disableMenuHeaderBandDebugLogging();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a game data set from Flutter packaged assets.
|
||||||
|
///
|
||||||
|
/// Intended for loading from dependency asset packages such as
|
||||||
|
/// `wolf_3d_assets` by passing [packageName] and [assetDirectory], then
|
||||||
|
/// supplying the result to [init] via [seededGames].
|
||||||
|
static Future<WolfensteinData> loadGameDataFromAssets({
|
||||||
|
required GameVersion version,
|
||||||
|
required String assetDirectory,
|
||||||
|
String? packageName = 'wolf_3d_assets',
|
||||||
|
AssetRegistry? registryOverride,
|
||||||
|
Wolf3dAssetByteLoader? assetLoader,
|
||||||
|
}) async {
|
||||||
|
final String ext = version.fileExtension;
|
||||||
|
final Wolf3dAssetByteLoader loader = assetLoader ?? rootBundle.load;
|
||||||
|
|
||||||
|
final String normalizedDirectory = assetDirectory.trim().replaceAll(
|
||||||
|
RegExp(r'^/+|/+$'),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
if (normalizedDirectory.isEmpty) {
|
||||||
|
throw ArgumentError.value(
|
||||||
|
assetDirectory,
|
||||||
|
'assetDirectory',
|
||||||
|
'Must not be empty.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String keyFor(String fileName) {
|
||||||
|
final path = '$normalizedDirectory/$fileName';
|
||||||
|
final String? normalizedPackage = packageName?.trim();
|
||||||
|
if (normalizedPackage == null || normalizedPackage.isEmpty) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return 'packages/$normalizedPackage/$path';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ByteData> loadRequired(String fileName) {
|
||||||
|
return loader(keyFor(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ByteData> loadWithFallback(
|
||||||
|
String primaryName,
|
||||||
|
String fallbackName,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await loadRequired(primaryName);
|
||||||
|
} catch (_) {
|
||||||
|
return loadRequired(fallbackName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WolfensteinLoader.loadFromBytes(
|
||||||
|
version: version,
|
||||||
|
vswap: await loadRequired('VSWAP.$ext'),
|
||||||
|
mapHead: await loadRequired('MAPHEAD.$ext'),
|
||||||
|
gameMaps: await loadWithFallback('GAMEMAPS.$ext', 'MAPTEMP.$ext'),
|
||||||
|
vgaDict: await loadRequired('VGADICT.$ext'),
|
||||||
|
vgaHead: await loadRequired('VGAHEAD.$ext'),
|
||||||
|
vgaGraph: await loadRequired('VGAGRAPH.$ext'),
|
||||||
|
audioHed: await loadRequired('AUDIOHED.$ext'),
|
||||||
|
audioT: await loadRequired('AUDIOT.$ext'),
|
||||||
|
registryOverride: registryOverride,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Initializes the engine by loading available game data.
|
/// Initializes the engine by loading available game data.
|
||||||
///
|
///
|
||||||
/// If [directory] is provided, it is persisted and treated as the primary
|
/// If [directory] is provided, it is persisted and treated as the primary
|
||||||
/// external search root. If omitted, a previously persisted directory is
|
/// external search root. If omitted, a previously persisted directory is
|
||||||
/// used when available. [additionalDirectories] are scanned after the
|
/// used when available. [additionalDirectories] are scanned after the
|
||||||
/// primary directory and are not persisted.
|
/// primary directory and are not persisted. [seededGames] are merged first,
|
||||||
|
/// enabling hosts to inject data from packaged assets.
|
||||||
///
|
///
|
||||||
/// This method scans only configured external directories, deduplicating
|
/// This method scans only configured external directories, deduplicating
|
||||||
/// discovered versions by [GameVersion]. Shared package code does not bundle
|
/// discovered versions by [GameVersion]. Shared package code does not bundle
|
||||||
@@ -64,10 +160,12 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
|||||||
Future<Wolf3dFlutterEngine> init({
|
Future<Wolf3dFlutterEngine> init({
|
||||||
String? directory,
|
String? directory,
|
||||||
Iterable<String>? additionalDirectories,
|
Iterable<String>? additionalDirectories,
|
||||||
|
Iterable<WolfensteinData>? seededGames,
|
||||||
}) async {
|
}) async {
|
||||||
await desktop_windowing_support.ensureDesktopWindowingInitialized();
|
await desktop_windowing_support.ensureDesktopWindowingInitialized();
|
||||||
await audio.init();
|
await audio.init();
|
||||||
availableGames.clear();
|
availableGames.clear();
|
||||||
|
_addUniqueGames(seededGames);
|
||||||
|
|
||||||
final String? requestedDirectory = directory?.trim();
|
final String? requestedDirectory = directory?.trim();
|
||||||
final String? resolvedDirectory =
|
final String? resolvedDirectory =
|
||||||
@@ -103,14 +201,7 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
|||||||
directoryPath: directoryPath,
|
directoryPath: directoryPath,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
);
|
);
|
||||||
for (final MapEntry<GameVersion, WolfensteinData> entry
|
_addUniqueGames(externalGames.values);
|
||||||
in externalGames.entries) {
|
|
||||||
if (!availableGames.any(
|
|
||||||
(WolfensteinData g) => g.version == entry.key,
|
|
||||||
)) {
|
|
||||||
availableGames.add(entry.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('External discovery failed: $e');
|
debugPrint('External discovery failed: $e');
|
||||||
}
|
}
|
||||||
@@ -119,4 +210,15 @@ class Wolf3dFlutterEngine extends Wolf3dEngine {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _addUniqueGames(Iterable<WolfensteinData>? games) {
|
||||||
|
if (games == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final WolfensteinData game in games) {
|
||||||
|
if (!availableGames.any((g) => g.version == game.version)) {
|
||||||
|
availableGames.add(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,16 @@ class WolfGlslRenderer extends BaseWolfRenderer {
|
|||||||
class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
||||||
static const int _renderWidth = 960;
|
static const int _renderWidth = 960;
|
||||||
static const int _renderHeight = 600;
|
static const int _renderHeight = 600;
|
||||||
|
static const String _baseShaderAsset =
|
||||||
|
'packages/wolf_3d_flutter/shaders/wolf_world.frag';
|
||||||
|
static const String _bloomShaderAsset =
|
||||||
|
'packages/wolf_3d_flutter/shaders/wolf_world_bloom.frag';
|
||||||
|
|
||||||
final SoftwareRenderer _renderer = SoftwareRenderer();
|
final SoftwareRenderer _renderer = SoftwareRenderer();
|
||||||
|
|
||||||
ui.Image? _renderedFrame;
|
ui.Image? _renderedFrame;
|
||||||
ui.FragmentProgram? _shaderProgram;
|
ui.FragmentShader? _baseShader;
|
||||||
ui.FragmentShader? _shader;
|
ui.FragmentShader? _bloomShader;
|
||||||
bool _isShaderUnavailable = false;
|
bool _isShaderUnavailable = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -99,7 +103,11 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
return const CircularProgressIndicator(color: Colors.white24);
|
return const CircularProgressIndicator(color: Colors.white24);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isShaderUnavailable || _shader == null) {
|
final ui.FragmentShader? activeShader = widget.bloomEnabled
|
||||||
|
? _bloomShader
|
||||||
|
: _baseShader;
|
||||||
|
|
||||||
|
if (_isShaderUnavailable || activeShader == null) {
|
||||||
// Keep frames visible even if GLSL initialization failed.
|
// Keep frames visible even if GLSL initialization failed.
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
@@ -117,9 +125,8 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: _GlslFramePainter(
|
painter: _GlslFramePainter(
|
||||||
frame: _renderedFrame!,
|
frame: _renderedFrame!,
|
||||||
shader: _shader!,
|
shader: activeShader,
|
||||||
effectsEnabled: widget.effectsEnabled,
|
effectsEnabled: widget.effectsEnabled,
|
||||||
bloomEnabled: widget.bloomEnabled,
|
|
||||||
elapsedSeconds: widget.engine.timeAliveMs / 1000.0,
|
elapsedSeconds: widget.engine.timeAliveMs / 1000.0,
|
||||||
),
|
),
|
||||||
child: const SizedBox.expand(),
|
child: const SizedBox.expand(),
|
||||||
@@ -130,15 +137,18 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
|
|
||||||
Future<void> _loadShader() async {
|
Future<void> _loadShader() async {
|
||||||
try {
|
try {
|
||||||
final ui.FragmentProgram program = await ui.FragmentProgram.fromAsset(
|
final List<ui.FragmentProgram> programs = await Future.wait(
|
||||||
'packages/wolf_3d_flutter/shaders/wolf_world.frag',
|
<Future<ui.FragmentProgram>>[
|
||||||
|
ui.FragmentProgram.fromAsset(_baseShaderAsset),
|
||||||
|
ui.FragmentProgram.fromAsset(_bloomShaderAsset),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_shaderProgram = program;
|
_baseShader = programs[0].fragmentShader();
|
||||||
_shader = _shaderProgram!.fragmentShader();
|
_bloomShader = programs[1].fragmentShader();
|
||||||
_isShaderUnavailable = false;
|
_isShaderUnavailable = false;
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -157,14 +167,12 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
final ui.Image frame;
|
final ui.Image frame;
|
||||||
final ui.FragmentShader shader;
|
final ui.FragmentShader shader;
|
||||||
final bool effectsEnabled;
|
final bool effectsEnabled;
|
||||||
final bool bloomEnabled;
|
|
||||||
final double elapsedSeconds;
|
final double elapsedSeconds;
|
||||||
|
|
||||||
_GlslFramePainter({
|
_GlslFramePainter({
|
||||||
required this.frame,
|
required this.frame,
|
||||||
required this.shader,
|
required this.shader,
|
||||||
required this.effectsEnabled,
|
required this.effectsEnabled,
|
||||||
required this.bloomEnabled,
|
|
||||||
required this.elapsedSeconds,
|
required this.elapsedSeconds,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,7 +187,6 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
..setFloat(3, texelY)
|
..setFloat(3, texelY)
|
||||||
..setFloat(4, effectsEnabled ? 1.0 : 0.0)
|
..setFloat(4, effectsEnabled ? 1.0 : 0.0)
|
||||||
..setFloat(5, elapsedSeconds)
|
..setFloat(5, elapsedSeconds)
|
||||||
..setFloat(6, bloomEnabled ? 1.0 : 0.0)
|
|
||||||
..setImageSampler(0, frame);
|
..setImageSampler(0, frame);
|
||||||
|
|
||||||
final Paint paint = Paint()
|
final Paint paint = Paint()
|
||||||
@@ -194,7 +201,6 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
return oldDelegate.frame != frame ||
|
return oldDelegate.frame != frame ||
|
||||||
oldDelegate.shader != shader ||
|
oldDelegate.shader != shader ||
|
||||||
oldDelegate.effectsEnabled != effectsEnabled ||
|
oldDelegate.effectsEnabled != effectsEnabled ||
|
||||||
oldDelegate.bloomEnabled != bloomEnabled ||
|
|
||||||
oldDelegate.elapsedSeconds != elapsedSeconds;
|
oldDelegate.elapsedSeconds != elapsedSeconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ dev_dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
shaders:
|
shaders:
|
||||||
- shaders/wolf_world.frag
|
- shaders/wolf_world.frag
|
||||||
|
- shaders/wolf_world_bloom.frag
|
||||||
# To add assets to your package, add an assets section, like this:
|
# To add assets to your package, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
|
|||||||
@@ -1,100 +1,91 @@
|
|||||||
#include <flutter/runtime_effect.glsl>
|
#include <flutter/runtime_effect.glsl>
|
||||||
|
|
||||||
// Output surface size in pixels.
|
// ----------------------------------------------------------------------------
|
||||||
|
// Base Variant (Branchless, No Bloom)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// This shader is the bloom-disabled base variant of the Wolf CRT post-process
|
||||||
|
// path.
|
||||||
|
//
|
||||||
|
// Why keep a separate base file?
|
||||||
|
// - Dart/CPU side can select this program when bloom is off.
|
||||||
|
// - That avoids bloom texture taps entirely in the hot path.
|
||||||
|
// - This is a direct performance optimization, not just stylistic separation.
|
||||||
|
//
|
||||||
|
// Why branchless math throughout?
|
||||||
|
// - Fragment programs run many pixels in lockstep.
|
||||||
|
// - Divergent control flow can reduce throughput.
|
||||||
|
// - Mask-based selection using mix/step/smoothstep keeps execution uniform.
|
||||||
|
//
|
||||||
|
// For Dart developers:
|
||||||
|
// - Think dataflow over fields of values, not object-level control logic.
|
||||||
|
// - Most operations transform scalar/vector fields per pixel.
|
||||||
|
// - Alternative outcomes are blended by masks instead of branching.
|
||||||
|
|
||||||
|
// Output surface size in pixels for the current draw call.
|
||||||
uniform vec2 uResolution;
|
uniform vec2 uResolution;
|
||||||
// One source-texel step in UV space: (1/width, 1/height).
|
// One source texel step in UV space: (1/width, 1/height).
|
||||||
|
// This keeps neighborhood kernels stable across resolutions.
|
||||||
uniform vec2 uTexel;
|
uniform vec2 uTexel;
|
||||||
// 1.0 enables CRT post-process effects, 0.0 keeps only base AA.
|
// 1.0 enables CRT warp/scanline stack, 0.0 keeps only base AA stack.
|
||||||
|
// Even though this is conceptually boolean, it is expressed as float so the
|
||||||
|
// shader can blend outcomes natively with mask math.
|
||||||
uniform float uEffectsEnabled;
|
uniform float uEffectsEnabled;
|
||||||
// Engine time in seconds used to animate scanline travel.
|
// Engine time in seconds used to animate scanline travel.
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
// 1.0 enables CRT phosphor bloom glow, 0.0 disables it.
|
|
||||||
uniform float uBloomEnabled;
|
|
||||||
// Source frame produced by the software renderer.
|
// Source frame produced by the software renderer.
|
||||||
uniform sampler2D uTexture;
|
uniform sampler2D uTexture;
|
||||||
|
|
||||||
out vec4 fragColor;
|
out vec4 fragColor;
|
||||||
|
|
||||||
// Perceptual brightness approximation used for edge detection.
|
// Perceptual brightness approximation for edge detection.
|
||||||
|
// This uses Rec.601-style luma weighting.
|
||||||
float luma(vec3 color) {
|
float luma(vec3 color) {
|
||||||
return dot(color, vec3(0.299, 0.587, 0.114));
|
return dot(color, vec3(0.299, 0.587, 0.114));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns 1.0 when uv is inside [0,1] on both axes, else 0.0.
|
||||||
|
// Implemented branchlessly as a product of step() tests.
|
||||||
|
float uvInsideMask(vec2 uv) {
|
||||||
|
vec2 lower = step(vec2(0.0), uv);
|
||||||
|
vec2 upper = step(uv, vec2(1.0));
|
||||||
|
return lower.x * lower.y * upper.x * upper.y;
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Convert fragment coordinates to normalized UV coordinates.
|
// Normalize destination pixel coordinate into UV space.
|
||||||
vec2 uv = FlutterFragCoord().xy / uResolution;
|
vec2 uv = FlutterFragCoord().xy / uResolution;
|
||||||
|
|
||||||
if (uEffectsEnabled > 0.5) {
|
// --------------------------------------------------------------------------
|
||||||
// Barrel-like warp to emulate curved CRT glass.
|
// 1) CRT warp selection (branchless)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// The centered radius term drives a mild barrel distortion.
|
||||||
vec2 centered = uv * 2.0 - 1.0;
|
vec2 centered = uv * 2.0 - 1.0;
|
||||||
float radius2 = dot(centered, centered);
|
float radius2 = dot(centered, centered);
|
||||||
centered *= 1.0 + radius2 * 0.045;
|
vec2 warpedUv = centered * (1.0 + radius2 * 0.045) * 0.5 + 0.5;
|
||||||
uv = centered * 0.5 + 0.5;
|
|
||||||
|
|
||||||
// Fill outside warped bounds with a darker consumer-TV charcoal bezel.
|
// Mix between linear and warped UV with a float mask.
|
||||||
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
|
// In Dart, this is conceptually: useWarped ? warpedUv : uv.
|
||||||
vec2 clampedUv = clamp(uv, 0.0, 1.0);
|
vec2 effectiveUv = mix(uv, warpedUv, uEffectsEnabled);
|
||||||
vec2 edgeDelta = uv - clampedUv;
|
|
||||||
float overflow = max(abs(edgeDelta.x), abs(edgeDelta.y));
|
|
||||||
|
|
||||||
// Sample near-edge scene colors and spread them onto the bezel.
|
// Clamp once to keep all downstream texture fetches in bounds.
|
||||||
vec2 inwardDir = normalize(-edgeDelta + vec2(1e-6));
|
vec2 sampleUv = clamp(effectiveUv, 0.0, 1.0);
|
||||||
vec2 bleedStep = vec2(uTexel.x * 1.6, uTexel.y * 1.6);
|
|
||||||
vec2 bleedUv1 = clamp(clampedUv + inwardDir * bleedStep, 0.0, 1.0);
|
|
||||||
vec2 bleedUv2 = clamp(clampedUv + inwardDir * bleedStep * 2.6, 0.0, 1.0);
|
|
||||||
vec2 bleedUv3 = clamp(clampedUv + inwardDir * bleedStep * 4.2, 0.0, 1.0);
|
|
||||||
vec3 edgeBleedColor =
|
|
||||||
texture(uTexture, clampedUv).rgb * 0.52 +
|
|
||||||
texture(uTexture, bleedUv1).rgb * 0.28 +
|
|
||||||
texture(uTexture, bleedUv2).rgb * 0.14 +
|
|
||||||
texture(uTexture, bleedUv3).rgb * 0.06;
|
|
||||||
float edgeBleedLuma = luma(edgeBleedColor);
|
|
||||||
|
|
||||||
// Approximate concave bezel depth by measuring how far this fragment is
|
// 1.0 for screen interior, 0.0 outside.
|
||||||
// from the emissive screen boundary in aspect-corrected UV space.
|
float insideMask = uvInsideMask(effectiveUv);
|
||||||
vec2 aspectScale = vec2(uResolution.x / max(uResolution.y, 1.0), 1.0);
|
// Bezel contributes only where warped coordinates leave the source screen.
|
||||||
float bezelDistance = length(edgeDelta * aspectScale);
|
float bezelMask = (1.0 - insideMask) * uEffectsEnabled;
|
||||||
|
|
||||||
// Corners receive less direct bleed because the nearest lit area is
|
// --------------------------------------------------------------------------
|
||||||
// diagonally offset, so attenuate glow toward corner regions.
|
// 2) Lightweight edge-aware AA
|
||||||
vec2 clampedCentered = clampedUv * 2.0 - 1.0;
|
// --------------------------------------------------------------------------
|
||||||
float cornerFactor = smoothstep(0.60, 1.15, length(clampedCentered));
|
// Sample center + cardinal neighbors.
|
||||||
|
vec4 centerSample = texture(uTexture, sampleUv);
|
||||||
|
vec3 sampleN = texture(uTexture, clamp(sampleUv + vec2(0.0, -uTexel.y), 0.0, 1.0)).rgb;
|
||||||
|
vec3 sampleS = texture(uTexture, clamp(sampleUv + vec2(0.0, uTexel.y), 0.0, 1.0)).rgb;
|
||||||
|
vec3 sampleE = texture(uTexture, clamp(sampleUv + vec2(uTexel.x, 0.0), 0.0, 1.0)).rgb;
|
||||||
|
vec3 sampleW = texture(uTexture, clamp(sampleUv + vec2(-uTexel.x, 0.0), 0.0, 1.0)).rgb;
|
||||||
|
|
||||||
float verticalShade = 0.88 + 0.07 * (1.0 - (FlutterFragCoord().y / uResolution.y));
|
// Luma span estimates local contrast; high span implies stronger edge.
|
||||||
float depthShade = 1.0 - smoothstep(0.0, 0.058, overflow) * 0.34;
|
|
||||||
float grain = sin(FlutterFragCoord().x * 0.21 + FlutterFragCoord().y * 0.11) * 0.006;
|
|
||||||
float moldedHighlight = smoothstep(0.072, 0.0, overflow) * 0.028;
|
|
||||||
|
|
||||||
// Deeper arcade-style profile: tighter, scene-tinted bleed rolloff.
|
|
||||||
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
|
|
||||||
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
|
|
||||||
float bleedStrength = smoothstep(0.12, 0.0, overflow) * (0.78 - cornerFactor * 0.26);
|
|
||||||
float bloomBezelBoost = 1.0 +
|
|
||||||
uBloomEnabled * smoothstep(0.16, 0.82, edgeBleedLuma) * 0.75;
|
|
||||||
float bloomLipBoost = 1.0 +
|
|
||||||
uBloomEnabled * smoothstep(0.10, 0.68, edgeBleedLuma) * 0.45;
|
|
||||||
|
|
||||||
vec3 bezelColor =
|
|
||||||
vec3(0.225, 0.225, 0.215) * verticalShade * depthShade +
|
|
||||||
edgeBleedColor * bezelGlow * bleedStrength * 1.12 * bloomBezelBoost +
|
|
||||||
edgeBleedColor * innerLip * 0.36 * bloomLipBoost +
|
|
||||||
vec3(moldedHighlight) +
|
|
||||||
vec3(grain);
|
|
||||||
fragColor = vec4(bezelColor, 1.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the base color from the source frame.
|
|
||||||
vec4 centerSample = texture(uTexture, uv);
|
|
||||||
|
|
||||||
// Sample 4-neighborhood (N/S/E/W) around the current pixel.
|
|
||||||
vec3 sampleN = texture(uTexture, uv + vec2(0.0, -uTexel.y)).rgb;
|
|
||||||
vec3 sampleS = texture(uTexture, uv + vec2(0.0, uTexel.y)).rgb;
|
|
||||||
vec3 sampleE = texture(uTexture, uv + vec2(uTexel.x, 0.0)).rgb;
|
|
||||||
vec3 sampleW = texture(uTexture, uv + vec2(-uTexel.x, 0.0)).rgb;
|
|
||||||
|
|
||||||
// Compute local luma range; wider range means a stronger edge.
|
|
||||||
float lumaCenter = luma(centerSample.rgb);
|
float lumaCenter = luma(centerSample.rgb);
|
||||||
float lumaMin = min(
|
float lumaMin = min(
|
||||||
lumaCenter,
|
lumaCenter,
|
||||||
@@ -104,72 +95,78 @@ void main() {
|
|||||||
lumaCenter,
|
lumaCenter,
|
||||||
max(max(luma(sampleN), luma(sampleS)), max(luma(sampleE), luma(sampleW)))
|
max(max(luma(sampleN), luma(sampleS)), max(luma(sampleE), luma(sampleW)))
|
||||||
);
|
);
|
||||||
|
float edgeAmount = smoothstep(0.03, 0.18, max(lumaMax - lumaMin, 0.0001));
|
||||||
float edgeSpan = max(lumaMax - lumaMin, 0.0001);
|
|
||||||
// Convert raw edge strength into a smooth 0..1 blending amount.
|
|
||||||
float edgeAmount = smoothstep(0.03, 0.18, edgeSpan);
|
|
||||||
|
|
||||||
// Average neighbors and blend toward that average only near edges.
|
|
||||||
// This acts like a lightweight edge-aware anti-aliasing pass.
|
|
||||||
vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25;
|
vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25;
|
||||||
|
|
||||||
|
// Blend toward neighborhood average near likely edges.
|
||||||
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
||||||
|
|
||||||
// Preserve source alpha and output the anti-aliased color.
|
// --------------------------------------------------------------------------
|
||||||
vec3 outColor = aaColor;
|
// 3) CRT scanline/sweep/vignette stack
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
if (uEffectsEnabled > 0.5) {
|
float scanlineBand = 0.88 + 0.12 * sin(sampleUv.y * uResolution.y * 3.14159265);
|
||||||
// Horizontal scanline modulation.
|
|
||||||
float scanlineBand = 0.88 + 0.12 * sin(uv.y * uResolution.y * 3.14159265);
|
|
||||||
|
|
||||||
// Slow bright line crawling down the screen.
|
|
||||||
float sweepPos = fract(uTime * 0.08);
|
float sweepPos = fract(uTime * 0.08);
|
||||||
float sweepBand = 1.0 + 0.16 * exp(-pow((uv.y - sweepPos) * 120.0, 2.0));
|
float sweepBand = 1.0 + 0.16 * exp(-pow((sampleUv.y - sweepPos) * 120.0, 2.0));
|
||||||
|
vec2 centeredUv = sampleUv * 2.0 - 1.0;
|
||||||
// Slight center brightening and edge falloff (CRT phosphor + lens feel).
|
|
||||||
vec2 centeredUv = uv * 2.0 - 1.0;
|
|
||||||
float vignette = smoothstep(1.15, 0.25, length(centeredUv));
|
float vignette = smoothstep(1.15, 0.25, length(centeredUv));
|
||||||
float centerLift = 1.0 + 0.08 * (1.0 - length(centeredUv));
|
float centerLift = 1.0 + 0.08 * (1.0 - length(centeredUv));
|
||||||
|
|
||||||
outColor *= scanlineBand * sweepBand * centerLift;
|
vec3 crtColor = aaColor;
|
||||||
outColor *= mix(0.62, 1.0, vignette);
|
crtColor *= scanlineBand * sweepBand * centerLift;
|
||||||
}
|
crtColor *= mix(0.62, 1.0, vignette);
|
||||||
|
|
||||||
if (uBloomEnabled > 0.5) {
|
// Effects mask decides whether CRT modulation is applied.
|
||||||
// CRT phosphor bloom: bright areas spread a soft luminance glow.
|
vec3 screenColor = mix(aaColor, crtColor, uEffectsEnabled);
|
||||||
// Sample a three-ring cross pattern directly from the source texture so
|
|
||||||
// the spread is measured in source-texel space and stays resolution-stable.
|
|
||||||
vec2 s1 = uTexel * 3.0;
|
|
||||||
vec2 s2 = uTexel * 7.0;
|
|
||||||
vec2 s3 = uTexel * 13.0;
|
|
||||||
|
|
||||||
vec3 glow = vec3(0.0);
|
// --------------------------------------------------------------------------
|
||||||
// Inner ring — weight 1.0 each
|
// 4) Bezel shading path (branchless selection)
|
||||||
glow += texture(uTexture, uv + vec2( s1.x, 0.0)).rgb;
|
// --------------------------------------------------------------------------
|
||||||
glow += texture(uTexture, uv + vec2(-s1.x, 0.0)).rgb;
|
// This base variant intentionally does not include bloom calculations.
|
||||||
glow += texture(uTexture, uv + vec2( 0.0, s1.y)).rgb;
|
|
||||||
glow += texture(uTexture, uv + vec2( 0.0, -s1.y)).rgb;
|
|
||||||
// Mid ring — weight 0.5 each
|
|
||||||
glow += texture(uTexture, uv + vec2( s2.x, 0.0)).rgb * 0.5;
|
|
||||||
glow += texture(uTexture, uv + vec2(-s2.x, 0.0)).rgb * 0.5;
|
|
||||||
glow += texture(uTexture, uv + vec2( 0.0, s2.y)).rgb * 0.5;
|
|
||||||
glow += texture(uTexture, uv + vec2( 0.0, -s2.y)).rgb * 0.5;
|
|
||||||
// Outer ring — weight 0.25 each
|
|
||||||
glow += texture(uTexture, uv + vec2( s3.x, 0.0)).rgb * 0.25;
|
|
||||||
glow += texture(uTexture, uv + vec2(-s3.x, 0.0)).rgb * 0.25;
|
|
||||||
glow += texture(uTexture, uv + vec2( 0.0, s3.y)).rgb * 0.25;
|
|
||||||
glow += texture(uTexture, uv + vec2( 0.0, -s3.y)).rgb * 0.25;
|
|
||||||
// Normalize: 4*1.0 + 4*0.5 + 4*0.25 = 7.0
|
|
||||||
glow /= 7.0;
|
|
||||||
|
|
||||||
// Only bright pixels contribute — gate the bloom contribution on luma.
|
// edgeDelta is non-zero when warp pushes UV outside source bounds.
|
||||||
float glowLuma = luma(glow);
|
vec2 edgeDelta = effectiveUv - sampleUv;
|
||||||
float bloomStrength = smoothstep(0.18, 0.82, glowLuma);
|
float overflow = max(abs(edgeDelta.x), abs(edgeDelta.y));
|
||||||
|
|
||||||
// Add bloom additively then apply a gentle Reinhard-style tone-map to
|
// Sample inward from clamped border to pull scene tint into bezel.
|
||||||
// prevent over-saturation while keeping dark areas clean.
|
vec2 inwardDir = normalize(-edgeDelta + vec2(1e-6));
|
||||||
outColor = outColor + glow * bloomStrength * 0.42;
|
vec2 bleedStep = vec2(uTexel.x * 1.6, uTexel.y * 1.6);
|
||||||
outColor = outColor / (outColor + vec3(0.75)) * 1.75;
|
vec2 bleedUv1 = clamp(sampleUv + inwardDir * bleedStep, 0.0, 1.0);
|
||||||
}
|
vec2 bleedUv2 = clamp(sampleUv + inwardDir * bleedStep * 2.6, 0.0, 1.0);
|
||||||
|
vec2 bleedUv3 = clamp(sampleUv + inwardDir * bleedStep * 4.2, 0.0, 1.0);
|
||||||
|
vec3 edgeBleedColor =
|
||||||
|
texture(uTexture, sampleUv).rgb * 0.52 +
|
||||||
|
texture(uTexture, bleedUv1).rgb * 0.28 +
|
||||||
|
texture(uTexture, bleedUv2).rgb * 0.14 +
|
||||||
|
texture(uTexture, bleedUv3).rgb * 0.06;
|
||||||
|
|
||||||
fragColor = vec4(outColor, centerSample.a);
|
// Aspect-corrected radial metrics for bezel falloff shaping.
|
||||||
|
vec2 aspectScale = vec2(uResolution.x / max(uResolution.y, 1.0), 1.0);
|
||||||
|
float bezelDistance = length(edgeDelta * aspectScale);
|
||||||
|
vec2 clampedCentered = sampleUv * 2.0 - 1.0;
|
||||||
|
float cornerFactor = smoothstep(0.60, 1.15, length(clampedCentered));
|
||||||
|
|
||||||
|
// Shading layers:
|
||||||
|
// - verticalShade: slight top/bottom tonal variance
|
||||||
|
// - depthShade: darkens with overflow depth
|
||||||
|
// - grain: subtle analog texture
|
||||||
|
// - moldedHighlight: narrow inner edge highlight
|
||||||
|
float verticalShade = 0.88 + 0.07 * (1.0 - (FlutterFragCoord().y / uResolution.y));
|
||||||
|
float depthShade = 1.0 - smoothstep(0.0, 0.058, overflow) * 0.34;
|
||||||
|
float grain = sin(FlutterFragCoord().x * 0.21 + FlutterFragCoord().y * 0.11) * 0.006;
|
||||||
|
float moldedHighlight = smoothstep(0.072, 0.0, overflow) * 0.028;
|
||||||
|
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
|
||||||
|
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
|
||||||
|
float bleedStrength = smoothstep(0.12, 0.0, overflow) * (0.78 - cornerFactor * 0.26);
|
||||||
|
|
||||||
|
vec3 bezelColor =
|
||||||
|
vec3(0.225, 0.225, 0.215) * verticalShade * depthShade +
|
||||||
|
edgeBleedColor * bezelGlow * bleedStrength * 1.12 +
|
||||||
|
edgeBleedColor * innerLip * 0.36 +
|
||||||
|
vec3(moldedHighlight) +
|
||||||
|
vec3(grain);
|
||||||
|
|
||||||
|
// Final branchless selection between emissive screen and bezel.
|
||||||
|
vec3 outColor = mix(screenColor, bezelColor, bezelMask);
|
||||||
|
float outAlpha = mix(centerSample.a, 1.0, bezelMask);
|
||||||
|
fragColor = vec4(outColor, outAlpha);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
#include <flutter/runtime_effect.glsl>
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Bloom Variant (Branchless)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// This shader is the bloom-enabled variant of the Wolf CRT post-process path.
|
||||||
|
//
|
||||||
|
// Why a separate file instead of one giant toggle-heavy shader?
|
||||||
|
// - Dart/CPU side chooses this program only when bloom is enabled.
|
||||||
|
// - When bloom is disabled, the renderer uses the base shader variant and
|
||||||
|
// avoids bloom texture taps entirely.
|
||||||
|
// - This is a deliberate performance decision: skipping work at pipeline
|
||||||
|
// selection time is usually faster than computing work and masking it out.
|
||||||
|
//
|
||||||
|
// Why so much branchless math?
|
||||||
|
// - GPU fragment execution runs many pixels in lockstep.
|
||||||
|
// - Divergent control flow reduces throughput.
|
||||||
|
// - We prefer mask-driven selection using mix/step/smoothstep.
|
||||||
|
//
|
||||||
|
// For Dart developers:
|
||||||
|
// - Think of this as vectorized numeric dataflow, not object-oriented logic.
|
||||||
|
// - Most values are scalar or vector fields over the whole screen.
|
||||||
|
// - We combine fields with interpolation/masks instead of choosing one code
|
||||||
|
// path with direct branching.
|
||||||
|
|
||||||
|
// Output surface size in pixels for the current draw call.
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
// One source texel step in UV space: (1/width, 1/height).
|
||||||
|
// This keeps kernel sizes stable across resolutions.
|
||||||
|
uniform vec2 uTexel;
|
||||||
|
// 1.0 enables CRT warp/scanline stack, 0.0 keeps only base AA stack.
|
||||||
|
// Even though this is a boolean concept, it is expressed as float because
|
||||||
|
// interpolation and mask blending are native operations in GLSL.
|
||||||
|
uniform float uEffectsEnabled;
|
||||||
|
// Engine time in seconds used to animate scanline travel.
|
||||||
|
uniform float uTime;
|
||||||
|
// Source frame produced by the software renderer.
|
||||||
|
uniform sampler2D uTexture;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
// Perceptual brightness approximation for edge and bloom gating.
|
||||||
|
// This is Rec.601-style luma weighting.
|
||||||
|
float luma(vec3 color) {
|
||||||
|
return dot(color, vec3(0.299, 0.587, 0.114));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns 1.0 when uv is inside [0,1] on both axes, else 0.0.
|
||||||
|
// Implemented branchlessly with step() products.
|
||||||
|
float uvInsideMask(vec2 uv) {
|
||||||
|
vec2 lower = step(vec2(0.0), uv);
|
||||||
|
vec2 upper = step(uv, vec2(1.0));
|
||||||
|
return lower.x * lower.y * upper.x * upper.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Normalize destination pixel coordinate into UV space.
|
||||||
|
vec2 uv = FlutterFragCoord().xy / uResolution;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 1) CRT warp selection (branchless)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// The centered radius term drives a mild barrel distortion.
|
||||||
|
vec2 centered = uv * 2.0 - 1.0;
|
||||||
|
float radius2 = dot(centered, centered);
|
||||||
|
vec2 warpedUv = centered * (1.0 + radius2 * 0.045) * 0.5 + 0.5;
|
||||||
|
|
||||||
|
// Mix between linear and warped UV with a float mask.
|
||||||
|
// In Dart, this is conceptually: useWarped ? warpedUv : uv.
|
||||||
|
vec2 effectiveUv = mix(uv, warpedUv, uEffectsEnabled);
|
||||||
|
|
||||||
|
// Clamp once to keep all downstream texture fetches in bounds.
|
||||||
|
vec2 sampleUv = clamp(effectiveUv, 0.0, 1.0);
|
||||||
|
|
||||||
|
// 1.0 for screen interior, 0.0 outside.
|
||||||
|
float insideMask = uvInsideMask(effectiveUv);
|
||||||
|
// Bezel contributes only where warped coordinates leave the source screen.
|
||||||
|
float bezelMask = (1.0 - insideMask) * uEffectsEnabled;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 2) Lightweight edge-aware AA
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Sample center + cardinal neighbors.
|
||||||
|
vec4 centerSample = texture(uTexture, sampleUv);
|
||||||
|
vec3 sampleN = texture(uTexture, clamp(sampleUv + vec2(0.0, -uTexel.y), 0.0, 1.0)).rgb;
|
||||||
|
vec3 sampleS = texture(uTexture, clamp(sampleUv + vec2(0.0, uTexel.y), 0.0, 1.0)).rgb;
|
||||||
|
vec3 sampleE = texture(uTexture, clamp(sampleUv + vec2(uTexel.x, 0.0), 0.0, 1.0)).rgb;
|
||||||
|
vec3 sampleW = texture(uTexture, clamp(sampleUv + vec2(-uTexel.x, 0.0), 0.0, 1.0)).rgb;
|
||||||
|
|
||||||
|
// Luma span estimates local contrast; high span implies stronger edge.
|
||||||
|
float lumaCenter = luma(centerSample.rgb);
|
||||||
|
float lumaMin = min(
|
||||||
|
lumaCenter,
|
||||||
|
min(min(luma(sampleN), luma(sampleS)), min(luma(sampleE), luma(sampleW)))
|
||||||
|
);
|
||||||
|
float lumaMax = max(
|
||||||
|
lumaCenter,
|
||||||
|
max(max(luma(sampleN), luma(sampleS)), max(luma(sampleE), luma(sampleW)))
|
||||||
|
);
|
||||||
|
float edgeAmount = smoothstep(0.03, 0.18, max(lumaMax - lumaMin, 0.0001));
|
||||||
|
vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25;
|
||||||
|
|
||||||
|
// Blend toward neighborhood average near likely edges.
|
||||||
|
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 3) CRT scanline/sweep/vignette stack
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
float scanlineBand = 0.88 + 0.12 * sin(sampleUv.y * uResolution.y * 3.14159265);
|
||||||
|
float sweepPos = fract(uTime * 0.08);
|
||||||
|
float sweepBand = 1.0 + 0.16 * exp(-pow((sampleUv.y - sweepPos) * 120.0, 2.0));
|
||||||
|
vec2 centeredUv = sampleUv * 2.0 - 1.0;
|
||||||
|
float vignette = smoothstep(1.15, 0.25, length(centeredUv));
|
||||||
|
float centerLift = 1.0 + 0.08 * (1.0 - length(centeredUv));
|
||||||
|
|
||||||
|
vec3 crtColor = aaColor;
|
||||||
|
crtColor *= scanlineBand * sweepBand * centerLift;
|
||||||
|
crtColor *= mix(0.62, 1.0, vignette);
|
||||||
|
|
||||||
|
// Effects mask decides whether CRT modulation is applied.
|
||||||
|
vec3 screenColor = mix(aaColor, crtColor, uEffectsEnabled);
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 4) Bloom (enabled by selecting this shader variant)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Three cross-shaped rings measured in source texels.
|
||||||
|
vec2 s1 = uTexel * 3.0;
|
||||||
|
vec2 s2 = uTexel * 7.0;
|
||||||
|
vec2 s3 = uTexel * 13.0;
|
||||||
|
|
||||||
|
// Accumulate weighted glow taps.
|
||||||
|
vec3 glow = vec3(0.0);
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( s1.x, 0.0), 0.0, 1.0)).rgb;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2(-s1.x, 0.0), 0.0, 1.0)).rgb;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( 0.0, s1.y), 0.0, 1.0)).rgb;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( 0.0, -s1.y), 0.0, 1.0)).rgb;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( s2.x, 0.0), 0.0, 1.0)).rgb * 0.5;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2(-s2.x, 0.0), 0.0, 1.0)).rgb * 0.5;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( 0.0, s2.y), 0.0, 1.0)).rgb * 0.5;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( 0.0, -s2.y), 0.0, 1.0)).rgb * 0.5;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( s3.x, 0.0), 0.0, 1.0)).rgb * 0.25;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2(-s3.x, 0.0), 0.0, 1.0)).rgb * 0.25;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( 0.0, s3.y), 0.0, 1.0)).rgb * 0.25;
|
||||||
|
glow += texture(uTexture, clamp(sampleUv + vec2( 0.0, -s3.y), 0.0, 1.0)).rgb * 0.25;
|
||||||
|
|
||||||
|
// Normalize sum (4*1.0 + 4*0.5 + 4*0.25 = 7.0).
|
||||||
|
glow /= 7.0;
|
||||||
|
|
||||||
|
// Gate bloom with luma to keep dark areas cleaner.
|
||||||
|
float bloomStrength = smoothstep(0.18, 0.82, luma(glow));
|
||||||
|
screenColor = screenColor + glow * bloomStrength * 0.42;
|
||||||
|
|
||||||
|
// Gentle tone map to roll off highlights.
|
||||||
|
screenColor = screenColor / (screenColor + vec3(0.75)) * 1.75;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 5) Bezel shading path (branchless selection)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// edgeDelta is non-zero when warp pushes UV outside source bounds.
|
||||||
|
vec2 edgeDelta = effectiveUv - sampleUv;
|
||||||
|
float overflow = max(abs(edgeDelta.x), abs(edgeDelta.y));
|
||||||
|
|
||||||
|
// Sample inward from clamped border to pull scene tint into bezel.
|
||||||
|
vec2 inwardDir = normalize(-edgeDelta + vec2(1e-6));
|
||||||
|
vec2 bleedStep = vec2(uTexel.x * 1.6, uTexel.y * 1.6);
|
||||||
|
vec2 bleedUv1 = clamp(sampleUv + inwardDir * bleedStep, 0.0, 1.0);
|
||||||
|
vec2 bleedUv2 = clamp(sampleUv + inwardDir * bleedStep * 2.6, 0.0, 1.0);
|
||||||
|
vec2 bleedUv3 = clamp(sampleUv + inwardDir * bleedStep * 4.2, 0.0, 1.0);
|
||||||
|
vec3 edgeBleedColor =
|
||||||
|
texture(uTexture, sampleUv).rgb * 0.52 +
|
||||||
|
texture(uTexture, bleedUv1).rgb * 0.28 +
|
||||||
|
texture(uTexture, bleedUv2).rgb * 0.14 +
|
||||||
|
texture(uTexture, bleedUv3).rgb * 0.06;
|
||||||
|
|
||||||
|
// Bezel luma drives bloom-biased boosts near bright edges.
|
||||||
|
float edgeBleedLuma = luma(edgeBleedColor);
|
||||||
|
|
||||||
|
// Aspect-corrected radial metrics for bezel falloff shaping.
|
||||||
|
vec2 aspectScale = vec2(uResolution.x / max(uResolution.y, 1.0), 1.0);
|
||||||
|
float bezelDistance = length(edgeDelta * aspectScale);
|
||||||
|
vec2 clampedCentered = sampleUv * 2.0 - 1.0;
|
||||||
|
float cornerFactor = smoothstep(0.60, 1.15, length(clampedCentered));
|
||||||
|
|
||||||
|
// Shading layers:
|
||||||
|
// - verticalShade: slight top/bottom tonal variance
|
||||||
|
// - depthShade: darkens with overflow depth
|
||||||
|
// - grain: subtle analog texture
|
||||||
|
// - moldedHighlight: narrow inner edge highlight
|
||||||
|
float verticalShade = 0.88 + 0.07 * (1.0 - (FlutterFragCoord().y / uResolution.y));
|
||||||
|
float depthShade = 1.0 - smoothstep(0.0, 0.058, overflow) * 0.34;
|
||||||
|
float grain = sin(FlutterFragCoord().x * 0.21 + FlutterFragCoord().y * 0.11) * 0.006;
|
||||||
|
float moldedHighlight = smoothstep(0.072, 0.0, overflow) * 0.028;
|
||||||
|
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
|
||||||
|
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
|
||||||
|
float bleedStrength = smoothstep(0.12, 0.0, overflow) * (0.78 - cornerFactor * 0.26);
|
||||||
|
|
||||||
|
// Bloom variant intentionally boosts bezel bleed near bright scene edges.
|
||||||
|
float bloomBezelBoost = 1.0 + smoothstep(0.16, 0.82, edgeBleedLuma) * 0.75;
|
||||||
|
float bloomLipBoost = 1.0 + smoothstep(0.10, 0.68, edgeBleedLuma) * 0.45;
|
||||||
|
|
||||||
|
vec3 bezelColor =
|
||||||
|
vec3(0.225, 0.225, 0.215) * verticalShade * depthShade +
|
||||||
|
edgeBleedColor * bezelGlow * bleedStrength * 1.12 * bloomBezelBoost +
|
||||||
|
edgeBleedColor * innerLip * 0.36 * bloomLipBoost +
|
||||||
|
vec3(moldedHighlight) +
|
||||||
|
vec3(grain);
|
||||||
|
|
||||||
|
// Final branchless selection between emissive screen and bezel.
|
||||||
|
vec3 outColor = mix(screenColor, bezelColor, bezelMask);
|
||||||
|
float outAlpha = mix(centerSample.a, 1.0, bezelMask);
|
||||||
|
fragColor = vec4(outColor, outAlpha);
|
||||||
|
}
|
||||||
@@ -94,6 +94,57 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test('init can seed available games from host-provided data', () async {
|
||||||
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
|
'wolf3d-seeded-games-',
|
||||||
|
);
|
||||||
|
addTearDown(() async {
|
||||||
|
if (await tempDir.exists()) {
|
||||||
|
await tempDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final persistence = DefaultGameDataDirectoryPersistence(
|
||||||
|
filePath: '${tempDir.path}/settings.json',
|
||||||
|
);
|
||||||
|
final wolf3d = Wolf3dFlutterEngine(
|
||||||
|
audioBackend: _NoopAudio(),
|
||||||
|
dataDirectoryPersistence: persistence,
|
||||||
|
);
|
||||||
|
final retail = _buildTestData(version: GameVersion.retail);
|
||||||
|
final shareware = _buildTestData(version: GameVersion.shareware);
|
||||||
|
|
||||||
|
await wolf3d.init(seededGames: [retail, shareware]);
|
||||||
|
|
||||||
|
expect(wolf3d.availableGames, hasLength(2));
|
||||||
|
expect(
|
||||||
|
wolf3d.availableGames.map((g) => g.version),
|
||||||
|
containsAll([GameVersion.retail, GameVersion.shareware]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadGameDataFromAssets prefixes keys for package assets', () async {
|
||||||
|
final requestedKeys = <String>[];
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
() => Wolf3dFlutterEngine.loadGameDataFromAssets(
|
||||||
|
version: GameVersion.retail,
|
||||||
|
assetDirectory: 'assets/retail',
|
||||||
|
packageName: 'wolf_3d_assets',
|
||||||
|
assetLoader: (String key) async {
|
||||||
|
requestedKeys.add(key);
|
||||||
|
throw Exception('Expected test stop after first key.');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
throwsException,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
requestedKeys,
|
||||||
|
contains('packages/wolf_3d_assets/assets/retail/VSWAP.WL6'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('GameScreen hides debug FAB when debug mode is disabled', (
|
testWidgets('GameScreen hides debug FAB when debug mode is disabled', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -168,7 +219,9 @@ class _TestWolf3d extends Wolf3dFlutterEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WolfensteinData _buildTestData() {
|
WolfensteinData _buildTestData({
|
||||||
|
GameVersion version = GameVersion.retail,
|
||||||
|
}) {
|
||||||
final wallGrid = List.generate(64, (_) => List.filled(64, 0));
|
final wallGrid = List.generate(64, (_) => List.filled(64, 0));
|
||||||
final objectGrid = List.generate(64, (_) => List.filled(64, 0));
|
final objectGrid = List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
@@ -181,9 +234,11 @@ WolfensteinData _buildTestData() {
|
|||||||
objectGrid[2][2] = MapObject.playerEast;
|
objectGrid[2][2] = MapObject.playerEast;
|
||||||
|
|
||||||
return WolfensteinData(
|
return WolfensteinData(
|
||||||
version: GameVersion.retail,
|
version: version,
|
||||||
dataVersion: DataVersion.unknown,
|
dataVersion: DataVersion.unknown,
|
||||||
registry: RetailAssetRegistry(),
|
registry: version == GameVersion.shareware
|
||||||
|
? SharewareAssetRegistry()
|
||||||
|
: RetailAssetRegistry(),
|
||||||
walls: [_sprite(1), _sprite(1), _sprite(2), _sprite(2)],
|
walls: [_sprite(1), _sprite(1), _sprite(2), _sprite(2)],
|
||||||
sprites: List.generate(436, (_) => _sprite(255)),
|
sprites: List.generate(436, (_) => _sprite(255)),
|
||||||
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
|
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||||
|
|||||||
Reference in New Issue
Block a user