aether-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/biome.json +51 -0
- package/bun.lock +192 -0
- package/index.ts +1 -0
- package/package.json +25 -0
- package/serve.ts +125 -0
- package/src/audio/AudioEngine.ts +61 -0
- package/src/components/Animator3D.ts +65 -0
- package/src/components/AudioSource.ts +26 -0
- package/src/components/BitmapText.ts +25 -0
- package/src/components/Camera.ts +33 -0
- package/src/components/CameraFollow.ts +5 -0
- package/src/components/Collider.ts +16 -0
- package/src/components/Components.test.ts +68 -0
- package/src/components/Light.ts +15 -0
- package/src/components/MeshRenderer.ts +58 -0
- package/src/components/ParticleEmitter.ts +59 -0
- package/src/components/RigidBody.ts +9 -0
- package/src/components/ShadowCaster.ts +3 -0
- package/src/components/SkinnedMeshRenderer.ts +25 -0
- package/src/components/SpriteAnimator.ts +42 -0
- package/src/components/SpriteRenderer.ts +26 -0
- package/src/components/Transform.test.ts +39 -0
- package/src/components/Transform.ts +54 -0
- package/src/core/AssetManager.ts +123 -0
- package/src/core/Input.test.ts +67 -0
- package/src/core/Input.ts +94 -0
- package/src/core/Scene.ts +24 -0
- package/src/core/SceneManager.ts +57 -0
- package/src/core/Storage.ts +161 -0
- package/src/desktop/SteamClient.ts +52 -0
- package/src/ecs/System.ts +11 -0
- package/src/ecs/World.test.ts +29 -0
- package/src/ecs/World.ts +149 -0
- package/src/index.ts +115 -0
- package/src/math/Color.ts +100 -0
- package/src/math/Vector2.ts +96 -0
- package/src/math/Vector3.ts +103 -0
- package/src/math/math.test.ts +168 -0
- package/src/renderer/GlowMaterial.ts +66 -0
- package/src/renderer/LitMaterial.ts +337 -0
- package/src/renderer/Material.test.ts +23 -0
- package/src/renderer/Material.ts +80 -0
- package/src/renderer/OcclusionMaterial.ts +43 -0
- package/src/renderer/ParticleMaterial.ts +66 -0
- package/src/renderer/Shader.ts +44 -0
- package/src/renderer/SkinnedLitMaterial.ts +55 -0
- package/src/renderer/WaterMaterial.ts +298 -0
- package/src/renderer/WebGLRenderer.ts +917 -0
- package/src/systems/Animation3DSystem.ts +148 -0
- package/src/systems/AnimationSystem.ts +58 -0
- package/src/systems/AudioSystem.ts +62 -0
- package/src/systems/LightingSystem.ts +114 -0
- package/src/systems/ParticleSystem.ts +278 -0
- package/src/systems/PhysicsSystem.ts +211 -0
- package/src/systems/Systems.test.ts +165 -0
- package/src/systems/TextSystem.ts +153 -0
- package/src/ui/AnimationEditor.tsx +639 -0
- package/src/ui/BottomPanel.tsx +443 -0
- package/src/ui/EntityExplorer.tsx +420 -0
- package/src/ui/GameState.ts +286 -0
- package/src/ui/Icons.tsx +239 -0
- package/src/ui/InventoryPanel.tsx +335 -0
- package/src/ui/PlayerHUD.tsx +250 -0
- package/src/ui/SpriteEditor.tsx +3241 -0
- package/src/ui/SpriteSheetManager.tsx +198 -0
- package/src/utils/GLTFLoader.ts +257 -0
- package/src/utils/ObjLoader.ts +81 -0
- package/src/utils/idb.ts +137 -0
- package/src/utils/packer.ts +85 -0
- package/test_obj.ts +12 -0
- package/tsconfig.json +21 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { Material } from "./Material";
|
|
2
|
+
|
|
3
|
+
export const LIT_SPRITE_VS = `
|
|
4
|
+
attribute vec4 aVertexPosition;
|
|
5
|
+
attribute vec2 aTextureCoord;
|
|
6
|
+
attribute vec3 aVertexNormal;
|
|
7
|
+
|
|
8
|
+
uniform mat4 uModelMatrix;
|
|
9
|
+
uniform mat4 uModelViewMatrix;
|
|
10
|
+
uniform mat4 uProjectionMatrix;
|
|
11
|
+
uniform vec2 uTexOffset;
|
|
12
|
+
uniform vec2 uTexScale;
|
|
13
|
+
|
|
14
|
+
varying highp vec2 vTextureCoord;
|
|
15
|
+
varying highp vec3 vWorldPos;
|
|
16
|
+
varying highp vec3 vNormal;
|
|
17
|
+
|
|
18
|
+
void main(void) {
|
|
19
|
+
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
|
|
20
|
+
vTextureCoord = (aTextureCoord * uTexScale) + uTexOffset;
|
|
21
|
+
|
|
22
|
+
// Compute world position for lighting calculations
|
|
23
|
+
vWorldPos = (uModelMatrix * aVertexPosition).xyz;
|
|
24
|
+
|
|
25
|
+
// Approximate normal transformation using ModelMatrix directly
|
|
26
|
+
// This is valid for Translation/Rotation uniform scaling
|
|
27
|
+
vNormal = normalize(mat3(uModelMatrix) * aVertexNormal);
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const LIT_SPRITE_FS = `
|
|
32
|
+
precision mediump float;
|
|
33
|
+
|
|
34
|
+
varying highp vec2 vTextureCoord;
|
|
35
|
+
varying highp vec3 vWorldPos;
|
|
36
|
+
varying highp vec3 vNormal;
|
|
37
|
+
|
|
38
|
+
uniform sampler2D uSampler;
|
|
39
|
+
uniform lowp vec4 uColor;
|
|
40
|
+
|
|
41
|
+
// Lighting Uniforms
|
|
42
|
+
uniform lowp vec3 uAmbientColor;
|
|
43
|
+
uniform int uActivePointLights;
|
|
44
|
+
uniform highp vec3 uLightPositions[16];
|
|
45
|
+
uniform lowp vec3 uLightColors[16];
|
|
46
|
+
uniform highp vec4 uLightParams[16]; // x: intensity, y: radius, z: physicalElevation, w: flickerIntensity
|
|
47
|
+
|
|
48
|
+
// Occlusion map for shadows and screen properties
|
|
49
|
+
uniform sampler2D uOcclusionSampler;
|
|
50
|
+
uniform highp vec2 uResolution;
|
|
51
|
+
uniform highp mat4 uViewMatrix;
|
|
52
|
+
uniform highp mat4 uProjectionMatrix;
|
|
53
|
+
|
|
54
|
+
// Particle Shading System
|
|
55
|
+
uniform int uActiveParticleLights;
|
|
56
|
+
uniform highp vec4 uParticleLightPositions[64];
|
|
57
|
+
uniform lowp vec3 uParticleLightColors[64];
|
|
58
|
+
|
|
59
|
+
// Global Time for dynamic effects
|
|
60
|
+
uniform highp float uTime;
|
|
61
|
+
|
|
62
|
+
// Interleaved Gradient Noise for Dithering
|
|
63
|
+
float IGN(highp vec2 fragCoord) {
|
|
64
|
+
vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
|
|
65
|
+
return fract(magic.z * fract(dot(fragCoord, magic.xy)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Tuneable random delay/decay pattern for natural gas flickering
|
|
69
|
+
float getFlicker(float time, vec3 pos) {
|
|
70
|
+
// Offset time radically per-lightsource based on prime-scaled coordinates
|
|
71
|
+
float localTimeOffset = pos.x * 137.0 + pos.y * 311.0;
|
|
72
|
+
float absoluteTime = time + localTimeOffset;
|
|
73
|
+
|
|
74
|
+
// Determine a distinct 10-second interval block for this exact lamppost
|
|
75
|
+
float decasecond = floor(absoluteTime * 0.1);
|
|
76
|
+
|
|
77
|
+
// Dice roll strictly once per block independently!
|
|
78
|
+
float dice = fract(sin(decasecond * 12.9898 + pos.x * 78.233) * 43758.5453);
|
|
79
|
+
|
|
80
|
+
// 1% chance to stutter (e.g. extremely seldom, averages 1 flutter every 16 minutes per lamp)
|
|
81
|
+
if (dice < 0.015) {
|
|
82
|
+
float localProgress = fract(absoluteTime * 0.1) * 10.0;
|
|
83
|
+
|
|
84
|
+
if (localProgress < 1.0) {
|
|
85
|
+
// Chaotic 1-second flutter fading out naturally
|
|
86
|
+
float noise = fract(sin(time * 50.0 + pos.y) * 43758.5453);
|
|
87
|
+
float envelope = 1.0 - localProgress;
|
|
88
|
+
return 1.0 - (noise * 0.7 * envelope);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return 1.0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void main(void) {
|
|
96
|
+
vec4 texColor = texture2D(uSampler, vTextureCoord);
|
|
97
|
+
if (texColor.a < 0.05) discard;
|
|
98
|
+
|
|
99
|
+
vec4 baseColor = texColor * uColor;
|
|
100
|
+
|
|
101
|
+
vec3 lightAdded = vec3(0.0);
|
|
102
|
+
vec3 specularAdded = vec3(0.0);
|
|
103
|
+
vec2 fragUV = gl_FragCoord.xy / uResolution;
|
|
104
|
+
|
|
105
|
+
// Unequivocal structural detection of 2D (Orthographic) vs 3D (Perspective) camera matrices natively.
|
|
106
|
+
// uProjectionMatrix[3][3] equals 1.0 purely for Orthographic, and 0.0 for Perspective.
|
|
107
|
+
float is2D = uProjectionMatrix[3][3] > 0.5 ? 1.0 : 0.0;
|
|
108
|
+
|
|
109
|
+
vec3 norm = normalize(vNormal);
|
|
110
|
+
// Fallback for 2D sprites without normal mappings: Bypass diffuse angle scaling safely
|
|
111
|
+
if (is2D > 0.5) {
|
|
112
|
+
norm = vec3(0.0, 0.0, 1.0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// View direction for Specular (approximate isometric camera angle roughly 45 deg down)
|
|
116
|
+
vec3 viewDir = normalize(vec3(0.0, 0.707, 0.707));
|
|
117
|
+
|
|
118
|
+
// A. CORE POINT LIGHTS (Heavy 12-pass Raymarching shadows)
|
|
119
|
+
for (int i = 0; i < 16; i++) {
|
|
120
|
+
if (i >= uActivePointLights) break;
|
|
121
|
+
|
|
122
|
+
vec3 lightPos = uLightPositions[i];
|
|
123
|
+
vec3 lightColor = uLightColors[i];
|
|
124
|
+
float intensity = uLightParams[i].x;
|
|
125
|
+
float radius = uLightParams[i].y;
|
|
126
|
+
float height = uLightParams[i].z;
|
|
127
|
+
float flickerIntensity = uLightParams[i].w;
|
|
128
|
+
|
|
129
|
+
// Authentic pseudo-3D coordinate reconstruction for robust volumetric 2.5D lighting!
|
|
130
|
+
vec3 physPos = vWorldPos;
|
|
131
|
+
|
|
132
|
+
if (is2D > 0.5) {
|
|
133
|
+
// Check if this is a standing sprite inherently sorted on Z, or a background/ground tile (-5.0)
|
|
134
|
+
if (vWorldPos.z > -4.0) {
|
|
135
|
+
// Standing Sprite! Reconstruct its true 2.5D physical footprint Anchor Y geographically from the internal Z-buffer formula.
|
|
136
|
+
float trueBaseY = -vWorldPos.z * 100.0;
|
|
137
|
+
|
|
138
|
+
// The geometric Altitude & Elevation of the exact pixel on the sheet!
|
|
139
|
+
float pixelElevation = max(0.0, vWorldPos.y - trueBaseY);
|
|
140
|
+
|
|
141
|
+
// We construct a TRUE geometric volumetric point using Footprint Y as topological distance, and pixel Altitude as Elevation Z!
|
|
142
|
+
physPos = vec3(vWorldPos.x, trueBaseY, pixelElevation);
|
|
143
|
+
} else {
|
|
144
|
+
// Flat Ground Tile (Z = -5.0). True geological Elevation is strictly 0.0 ground plane.
|
|
145
|
+
physPos = vec3(vWorldPos.x, vWorldPos.y, 0.0);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
vec3 diff3D = lightPos - physPos; // true physical direction un-normalized
|
|
150
|
+
float distSq = dot(diff3D, diff3D);
|
|
151
|
+
float radiusSq = radius * radius;
|
|
152
|
+
|
|
153
|
+
if (distSq < radiusSq) {
|
|
154
|
+
// Prevent NaN glitches where geometric center aligns flawlessly with a pixel
|
|
155
|
+
float dist = max(0.001, sqrt(distSq));
|
|
156
|
+
vec3 lightDir = diff3D / dist;
|
|
157
|
+
float normDist = dist / radius;
|
|
158
|
+
|
|
159
|
+
// --- Spotlight Cone Directionality ---
|
|
160
|
+
// A real lamppost has a cap and physically points DOWN and outward natively.
|
|
161
|
+
// lightDir.z tracks downward angle perfectly natively in our 2.5D projection!
|
|
162
|
+
// If pixel is directly below (lightDir.z is large positive), weight is 1.0.
|
|
163
|
+
// If pixel is horizontal canopy (lightDir.z ~ 0.0), weight smoothly fades to 0.0.
|
|
164
|
+
float spotlightCone = smoothstep(0.05, 0.4, lightDir.z);
|
|
165
|
+
|
|
166
|
+
// --- Reflectance & Specular ---
|
|
167
|
+
float diffuse = dot(norm, lightDir) * 0.5 + 0.5;
|
|
168
|
+
vec3 specular = vec3(0.0);
|
|
169
|
+
|
|
170
|
+
if (is2D > 0.5) {
|
|
171
|
+
// Disable pure mathematical 3D specular for flat 2D billboard sprites.
|
|
172
|
+
// This prevents intense radioactive blowouts when isometric angles perfectly align with height offsets.
|
|
173
|
+
if (vWorldPos.z < -4.0) {
|
|
174
|
+
// Tile Ground Plane (Z=-5 internal rendering depth). Inject pseudo-texture bump mapping.
|
|
175
|
+
float bump = fract(sin(dot(floor(vWorldPos.xy * 16.0), vec2(12.9898, 78.233))) * 43758.5453);
|
|
176
|
+
diffuse = mix(0.9, 0.7 + bump * 0.4, clamp(1.0 - normDist, 0.0, 1.0));
|
|
177
|
+
} else {
|
|
178
|
+
// 100% stable standing 2D Sprite. Use static Half-Lambert facing up to receive
|
|
179
|
+
// soft volume lighting that fades uniformly along the edges of the light cone!
|
|
180
|
+
diffuse = 0.6 + (dot(vec3(0,0,1), lightDir) * 0.4);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Authentic 3D Mesh rendering (e.g. Dungeon walls). Execute true structural Specular
|
|
184
|
+
vec3 halfVector = normalize(lightDir + viewDir);
|
|
185
|
+
float spec = pow(max(dot(norm, halfVector), 0.0), 16.0); // Shininess = 16.0
|
|
186
|
+
specular = spec * lightColor * intensity * 0.8;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Gaussian soft falloff for natural glowing
|
|
190
|
+
float attenuation = max(0.0, (exp(-normDist * normDist * 4.0) - 0.0183) / 0.9817);
|
|
191
|
+
|
|
192
|
+
// Incorporate Downward Cone (Spotlight Cutoff) for 2D sprites.
|
|
193
|
+
// In pure 3D scenes (like Dungeon), we might want spherical lights. We dynamically apply cone
|
|
194
|
+
// ONLY if is2D > 0.5 because the cone is essential to prevent horizontal Canopy bleeding in 2D overlays!
|
|
195
|
+
if (is2D > 0.5) {
|
|
196
|
+
// If lightDir.z = 1.0 (looking straight down), weight is 1.0.
|
|
197
|
+
// If lightDir.z < 0.6 (looking primarily horizontally across the scene at tall geometries), weight drops permanently to 0.0!
|
|
198
|
+
float spotlightCone = smoothstep(0.55, 0.85, lightDir.z);
|
|
199
|
+
attenuation *= spotlightCone;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Natural, tuneable delay/decay flicker pattern mixed per-light
|
|
203
|
+
float flickerAmount = getFlicker(uTime, lightPos);
|
|
204
|
+
attenuation *= mix(1.0, flickerAmount, flickerIntensity);
|
|
205
|
+
|
|
206
|
+
float shadowMultiplier = 1.0;
|
|
207
|
+
|
|
208
|
+
// Disable screen-space raymarch shadows for Point Lights in heavily 2D Isometric modes.
|
|
209
|
+
// Screen-space Z-depth occlusion inherently destroys 2.5D lighting mathematics
|
|
210
|
+
// by forcing upright billboard sprites to cast infinite absolute black shadows linearly across the ground plane.
|
|
211
|
+
if (is2D < 0.5) {
|
|
212
|
+
// RAYMARCH SHADOWS:
|
|
213
|
+
vec4 lClip = uProjectionMatrix * uViewMatrix * vec4(lightPos, 1.0);
|
|
214
|
+
vec2 lightScreenUV = (lClip.xy / lClip.w) * 0.5 + 0.5;
|
|
215
|
+
|
|
216
|
+
vec2 rayVec = lightScreenUV - fragUV;
|
|
217
|
+
float stepsNum = 12.0;
|
|
218
|
+
vec2 stepUV = rayVec / stepsNum;
|
|
219
|
+
|
|
220
|
+
vec2 sampleUV = fragUV;
|
|
221
|
+
sampleUV += stepUV * (IGN(gl_FragCoord.xy) - 0.5);
|
|
222
|
+
|
|
223
|
+
for (int k = 0; k < 12; k++) {
|
|
224
|
+
sampleUV += stepUV;
|
|
225
|
+
if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
vec4 occData = texture2D(uOcclusionSampler, sampleUV);
|
|
229
|
+
float isOccluder = 1.0 - occData.b;
|
|
230
|
+
|
|
231
|
+
if (isOccluder > 0.5) {
|
|
232
|
+
float normZ = occData.r + occData.g * (1.0/255.0);
|
|
233
|
+
float occluderZ = normZ * 20.0 - 10.0;
|
|
234
|
+
// For 3D geometry ONLY, cast dynamic shadows downward
|
|
235
|
+
if (occluderZ > vWorldPos.z + 0.005) {
|
|
236
|
+
shadowMultiplier = 0.0;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Accumulate
|
|
244
|
+
lightAdded += lightColor * intensity * attenuation * diffuse * shadowMultiplier;
|
|
245
|
+
specularAdded += specular * attenuation * shadowMultiplier;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// B. SECONDARY PARTICLE LIGHTS (Fast Path - No shadows, specific radius)
|
|
250
|
+
vec3 particleAdded = vec3(0.0);
|
|
251
|
+
|
|
252
|
+
for (int j = 0; j < 64; j++) {
|
|
253
|
+
if (j >= uActiveParticleLights) break;
|
|
254
|
+
|
|
255
|
+
vec3 pLightPos = uParticleLightPositions[j].xyz;
|
|
256
|
+
float pRadius = uParticleLightPositions[j].w;
|
|
257
|
+
vec3 pLightColor = uParticleLightColors[j]; // Already scaled by intensity
|
|
258
|
+
|
|
259
|
+
vec3 posToLight = vWorldPos;
|
|
260
|
+
if (is2D > 0.5) {
|
|
261
|
+
// Terraria-style pixelated attenuation
|
|
262
|
+
// Assuming 1 unit = 1 tile = 16 pixels. Quantize position logically to grid.
|
|
263
|
+
posToLight = floor(vWorldPos * 16.0) / 16.0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
vec3 pDiff3D = pLightPos - posToLight;
|
|
267
|
+
if (is2D > 0.5) {
|
|
268
|
+
// Particles assume a slight consistent height over the 2D plane
|
|
269
|
+
pDiff3D.z = 0.5;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
float pDistSq = dot(pDiff3D, pDiff3D);
|
|
273
|
+
float pRadiusSq = pRadius * pRadius;
|
|
274
|
+
|
|
275
|
+
if (pDistSq < pRadiusSq) {
|
|
276
|
+
float pDist = sqrt(pDistSq);
|
|
277
|
+
vec3 pLightDir = pDiff3D / pDist;
|
|
278
|
+
|
|
279
|
+
// Half-Lambert
|
|
280
|
+
float pDiffuse = dot(norm, pLightDir) * 0.5 + 0.5;
|
|
281
|
+
if (is2D > 0.5) pDiffuse = 1.0;
|
|
282
|
+
|
|
283
|
+
// Gaussian soft falloff
|
|
284
|
+
float pNormDist = pDist / pRadius;
|
|
285
|
+
float pAtten = max(0.0, (exp(-pNormDist * pNormDist * 4.0) - 0.0183) / 0.9817);
|
|
286
|
+
|
|
287
|
+
// pure consistent particle light radius logic, particles don't independently sputter
|
|
288
|
+
|
|
289
|
+
if (is2D > 0.5) {
|
|
290
|
+
// Harsh visual steps
|
|
291
|
+
pAtten = floor(pAtten * 6.0) / 6.0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
particleAdded += pLightColor * pAtten * pDiffuse;
|
|
295
|
+
|
|
296
|
+
// Slight specular for particles too!
|
|
297
|
+
vec3 pHalfVector = normalize(pLightDir + viewDir);
|
|
298
|
+
float pSpec = pow(max(dot(norm, pHalfVector), 0.0), 32.0); // very shiny points
|
|
299
|
+
specularAdded += pSpec * pLightColor * pAtten * 0.4;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 1. Multiply Blend for ambient context (shadows)
|
|
304
|
+
vec3 shadedBase = baseColor.rgb * uAmbientColor;
|
|
305
|
+
|
|
306
|
+
// 2. Diffuse mapped light
|
|
307
|
+
vec3 mappedLight = baseColor.rgb * lightAdded;
|
|
308
|
+
|
|
309
|
+
// 3. To rescue extremely dark flat ground textures from absorbing illumination invisibly,
|
|
310
|
+
// we softly inject pure light geometrically. We STRICTLY MUST ONLY do this for Ground Tiles
|
|
311
|
+
// (Z < -4) rather than structural sprites like Trees, because Trees possess real color matrices
|
|
312
|
+
// that detonate into neon radioactive glitches when synthetic inverse-glow is stacked!
|
|
313
|
+
vec3 ambientGlow = vec3(0.0);
|
|
314
|
+
if (is2D > 0.5 && vWorldPos.z < -4.0) {
|
|
315
|
+
vec3 safeGlowEnergy = min(lightAdded, vec3(1.1));
|
|
316
|
+
ambientGlow = safeGlowEnergy * (vec3(1.0) - baseColor.rgb) * 0.9;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
vec3 litColor = shadedBase + mappedLight + (baseColor.rgb * particleAdded) + specularAdded + ambientGlow + (particleAdded * 0.1);
|
|
320
|
+
|
|
321
|
+
// No global curve tone mapping. The engine runs naturally unbounded and cleanly handles standard overflow.
|
|
322
|
+
litColor = clamp(litColor, 0.0, 1.0);
|
|
323
|
+
|
|
324
|
+
// Screen-space Dithering to completely eliminate 8-bit color banding
|
|
325
|
+
float dither = IGN(gl_FragCoord.xy) * (1.0 / 255.0);
|
|
326
|
+
litColor += dither - (0.5 / 255.0);
|
|
327
|
+
|
|
328
|
+
gl_FragColor = vec4(litColor, baseColor.a);
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
export class LitMaterial extends Material {
|
|
333
|
+
constructor() {
|
|
334
|
+
super(LIT_SPRITE_VS, LIT_SPRITE_FS);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { Color } from "../math/Color";
|
|
3
|
+
import { Material } from "./Material";
|
|
4
|
+
|
|
5
|
+
describe("Material System", () => {
|
|
6
|
+
test("Program hashing", () => {
|
|
7
|
+
const mat1 = new Material("VS", "FS");
|
|
8
|
+
const mat2 = new Material("VS", "FS");
|
|
9
|
+
const mat3 = new Material("VS2", "FS");
|
|
10
|
+
|
|
11
|
+
expect(mat1.programHash).toBe(mat2.programHash);
|
|
12
|
+
expect(mat1.programHash).not.toBe(mat3.programHash);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("Uniforms storage", () => {
|
|
16
|
+
const mat = new Material("VS", "FS");
|
|
17
|
+
mat.setUniform("uColor", new Color(255, 0, 0));
|
|
18
|
+
mat.setUniform("uIntensity", 1.5);
|
|
19
|
+
|
|
20
|
+
expect(mat.uniforms.uColor).toBeInstanceOf(Color);
|
|
21
|
+
expect(mat.uniforms.uIntensity).toBe(1.5);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { TextureAsset } from "../core/AssetManager";
|
|
2
|
+
import type { Color } from "../math/Color";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MESH_VS = `
|
|
5
|
+
attribute vec4 aVertexPosition;
|
|
6
|
+
attribute vec4 aVertexColor;
|
|
7
|
+
uniform mat4 uModelViewMatrix;
|
|
8
|
+
uniform mat4 uProjectionMatrix;
|
|
9
|
+
varying lowp vec4 vColor;
|
|
10
|
+
void main(void) {
|
|
11
|
+
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
|
|
12
|
+
vColor = aVertexColor;
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_MESH_FS = `
|
|
17
|
+
varying lowp vec4 vColor;
|
|
18
|
+
void main(void) {
|
|
19
|
+
gl_FragColor = vColor;
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_SPRITE_VS = `
|
|
24
|
+
attribute vec4 aVertexPosition;
|
|
25
|
+
attribute vec2 aTextureCoord;
|
|
26
|
+
uniform mat4 uModelViewMatrix;
|
|
27
|
+
uniform mat4 uProjectionMatrix;
|
|
28
|
+
uniform vec2 uTexOffset;
|
|
29
|
+
uniform vec2 uTexScale;
|
|
30
|
+
varying highp vec2 vTextureCoord;
|
|
31
|
+
void main(void) {
|
|
32
|
+
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
|
|
33
|
+
vTextureCoord = (aTextureCoord * uTexScale) + uTexOffset;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_SPRITE_FS = `
|
|
38
|
+
precision mediump float;
|
|
39
|
+
varying highp vec2 vTextureCoord;
|
|
40
|
+
uniform sampler2D uSampler;
|
|
41
|
+
uniform lowp vec4 uColor;
|
|
42
|
+
void main(void) {
|
|
43
|
+
vec4 texColor = texture2D(uSampler, vTextureCoord);
|
|
44
|
+
if (texColor.a < 0.05) discard;
|
|
45
|
+
gl_FragColor = texColor * uColor;
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
export type UniformValue =
|
|
50
|
+
| number
|
|
51
|
+
| number[]
|
|
52
|
+
| Float32Array
|
|
53
|
+
| Int32Array
|
|
54
|
+
| Color
|
|
55
|
+
| TextureAsset
|
|
56
|
+
| any;
|
|
57
|
+
|
|
58
|
+
export class Material {
|
|
59
|
+
public vertexShader: string;
|
|
60
|
+
public fragmentShader: string;
|
|
61
|
+
public uniforms: Record<string, UniformValue> = {};
|
|
62
|
+
public transparent: boolean = false;
|
|
63
|
+
public depthTest: boolean = true;
|
|
64
|
+
public depthWrite: boolean = true;
|
|
65
|
+
public cullFace: boolean = true;
|
|
66
|
+
public blendMode: "Alpha" | "Additive" | "Screen" = "Alpha";
|
|
67
|
+
|
|
68
|
+
public get programHash(): string {
|
|
69
|
+
return `${this.vertexShader}|${this.fragmentShader}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
constructor(vs: string, fs: string) {
|
|
73
|
+
this.vertexShader = vs;
|
|
74
|
+
this.fragmentShader = fs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setUniform(name: string, value: UniformValue) {
|
|
78
|
+
this.uniforms[name] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Material } from "./Material";
|
|
2
|
+
import { LIT_SPRITE_VS } from "./LitMaterial";
|
|
3
|
+
|
|
4
|
+
export const OCCLUSION_FS = `
|
|
5
|
+
precision mediump float;
|
|
6
|
+
|
|
7
|
+
varying highp vec2 vTextureCoord;
|
|
8
|
+
varying highp vec3 vWorldPos; // Must match vertex shader exactly
|
|
9
|
+
|
|
10
|
+
uniform sampler2D uSampler;
|
|
11
|
+
uniform lowp vec4 uColor;
|
|
12
|
+
uniform highp vec2 uTexOffset;
|
|
13
|
+
uniform highp vec2 uTexScale;
|
|
14
|
+
|
|
15
|
+
void main(void) {
|
|
16
|
+
vec4 texColor = texture2D(uSampler, vTextureCoord);
|
|
17
|
+
if (texColor.a < 0.1 || uColor.a < 0.1) discard;
|
|
18
|
+
|
|
19
|
+
// Only cast shadows from the bottom 25% of the sprite
|
|
20
|
+
// This creates a footprint-based shadow in 2D top-down instead of a full-body shadow,
|
|
21
|
+
// which prevents objects (like trees) from heavily self-shadowing their own canopies
|
|
22
|
+
vec2 localUV = (vTextureCoord - uTexOffset) / uTexScale;
|
|
23
|
+
if (localUV.y < 0.75) discard;
|
|
24
|
+
|
|
25
|
+
// Output pure black (with alpha) for the occlusion map.
|
|
26
|
+
// We encode the Z-depth of the occluder into the Red and Green channels to support depth-aware shadows.
|
|
27
|
+
// Z is mapped from [-10.0, 10.0] to [0.0, 1.0].
|
|
28
|
+
float normZ = clamp((vWorldPos.z + 10.0) / 20.0, 0.0, 1.0);
|
|
29
|
+
vec2 enc = vec2(1.0, 255.0) * normZ;
|
|
30
|
+
enc = fract(enc);
|
|
31
|
+
enc.x -= enc.y * (1.0/255.0);
|
|
32
|
+
|
|
33
|
+
// We use Blue = 0.0 to indicate this is an occluder, since clear color is White (1.0, 1.0, 1.0, 1.0).
|
|
34
|
+
gl_FragColor = vec4(enc.x, enc.y, 0.0, 1.0);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export class OcclusionMaterial extends Material {
|
|
39
|
+
constructor() {
|
|
40
|
+
// We can safely reuse the standard LIT_SPRITE_VS as it just passes position and scale
|
|
41
|
+
super(LIT_SPRITE_VS, OCCLUSION_FS);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Material } from "./Material";
|
|
2
|
+
|
|
3
|
+
export const PARTICLE_VS = `
|
|
4
|
+
attribute vec4 aVertexPosition;
|
|
5
|
+
attribute vec2 aTextureCoord;
|
|
6
|
+
attribute vec4 aVertexColor; // Need color buffer mapped!
|
|
7
|
+
|
|
8
|
+
uniform mat4 uModelMatrix;
|
|
9
|
+
uniform mat4 uModelViewMatrix;
|
|
10
|
+
uniform mat4 uProjectionMatrix;
|
|
11
|
+
|
|
12
|
+
varying highp vec2 vTextureCoord;
|
|
13
|
+
varying lowp vec4 vColor;
|
|
14
|
+
|
|
15
|
+
void main(void) {
|
|
16
|
+
// Note: since ParticleSystem calculates world or relative positions
|
|
17
|
+
// locally if it is handling billboarding, uModelViewMatrix covers camera projection cleanly
|
|
18
|
+
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
|
|
19
|
+
vTextureCoord = aTextureCoord;
|
|
20
|
+
vColor = aVertexColor;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const PARTICLE_FS = `
|
|
25
|
+
precision mediump float;
|
|
26
|
+
|
|
27
|
+
varying highp vec2 vTextureCoord;
|
|
28
|
+
varying lowp vec4 vColor;
|
|
29
|
+
|
|
30
|
+
uniform sampler2D uSampler;
|
|
31
|
+
uniform int uUseTexture;
|
|
32
|
+
uniform int uShape; // 0: square, 1: circle
|
|
33
|
+
|
|
34
|
+
void main(void) {
|
|
35
|
+
vec4 texColor = vec4(1.0);
|
|
36
|
+
|
|
37
|
+
if (uUseTexture == 1) {
|
|
38
|
+
texColor = texture2D(uSampler, vTextureCoord);
|
|
39
|
+
} else if (uShape == 1) {
|
|
40
|
+
// Procedural soft circle glow
|
|
41
|
+
vec2 center = vec2(0.5, 0.5);
|
|
42
|
+
float dist = distance(vTextureCoord, center);
|
|
43
|
+
|
|
44
|
+
// Soft gradient fade based on distance (fixed undefined edge boundaries)
|
|
45
|
+
float glow = 1.0 - smoothstep(0.0, 0.5, dist);
|
|
46
|
+
texColor = vec4(1.0, 1.0, 1.0, glow);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
vec4 finalColor = texColor * vColor;
|
|
50
|
+
|
|
51
|
+
if (finalColor.a < 0.01) discard;
|
|
52
|
+
|
|
53
|
+
gl_FragColor = finalColor;
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
export class ParticleMaterial extends Material {
|
|
58
|
+
constructor() {
|
|
59
|
+
super(PARTICLE_VS, PARTICLE_FS);
|
|
60
|
+
this.uniforms.uUseTexture = new Int32Array([0]);
|
|
61
|
+
this.uniforms.uShape = new Int32Array([1]); // default to circle
|
|
62
|
+
this.blendMode = "Additive";
|
|
63
|
+
this.depthTest = true;
|
|
64
|
+
this.depthWrite = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class Shader {
|
|
2
|
+
public program: WebGLProgram;
|
|
3
|
+
|
|
4
|
+
constructor(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
|
|
5
|
+
const vertexShader = this.compileShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
6
|
+
const fragmentShader = this.compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
7
|
+
|
|
8
|
+
const program = gl.createProgram();
|
|
9
|
+
if (!program) throw new Error("Failed to create WebGL program");
|
|
10
|
+
|
|
11
|
+
gl.attachShader(program, vertexShader);
|
|
12
|
+
gl.attachShader(program, fragmentShader);
|
|
13
|
+
gl.linkProgram(program);
|
|
14
|
+
|
|
15
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Unable to initialize the shader program: " +
|
|
18
|
+
gl.getProgramInfoLog(program),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.program = program;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private compileShader(
|
|
26
|
+
gl: WebGLRenderingContext,
|
|
27
|
+
type: number,
|
|
28
|
+
source: string,
|
|
29
|
+
): WebGLShader {
|
|
30
|
+
const shader = gl.createShader(type);
|
|
31
|
+
if (!shader) throw new Error("Failed to create WebGL shader");
|
|
32
|
+
|
|
33
|
+
gl.shaderSource(shader, source);
|
|
34
|
+
gl.compileShader(shader);
|
|
35
|
+
|
|
36
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
37
|
+
const info = gl.getShaderInfoLog(shader);
|
|
38
|
+
document.body.innerHTML += `<div style="color:red;font-size:24px;position:absolute;z-index:9999;top:0;left:0">${info}</div>`;
|
|
39
|
+
gl.deleteShader(shader);
|
|
40
|
+
throw new Error("An error occurred compiling the shaders: " + info);
|
|
41
|
+
}
|
|
42
|
+
return shader;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { LIT_SPRITE_FS, LIT_SPRITE_VS } from "./LitMaterial";
|
|
2
|
+
import { Material } from "./Material";
|
|
3
|
+
|
|
4
|
+
export const SKINNED_LIT_VS = `
|
|
5
|
+
attribute vec4 aVertexPosition;
|
|
6
|
+
attribute vec2 aTextureCoord;
|
|
7
|
+
attribute vec3 aVertexNormal;
|
|
8
|
+
|
|
9
|
+
// Skinning variables
|
|
10
|
+
attribute vec4 aJoints;
|
|
11
|
+
attribute vec4 aWeights;
|
|
12
|
+
uniform mat4 uJointMatrices[64];
|
|
13
|
+
|
|
14
|
+
uniform mat4 uModelMatrix;
|
|
15
|
+
uniform mat4 uModelViewMatrix;
|
|
16
|
+
uniform mat4 uProjectionMatrix;
|
|
17
|
+
uniform vec2 uTexOffset;
|
|
18
|
+
uniform vec2 uTexScale;
|
|
19
|
+
|
|
20
|
+
varying highp vec2 vTextureCoord;
|
|
21
|
+
varying highp vec3 vWorldPos;
|
|
22
|
+
varying highp vec3 vNormal;
|
|
23
|
+
|
|
24
|
+
void main(void) {
|
|
25
|
+
// 1. Calculate Skin Matrix
|
|
26
|
+
mat4 skinMatrix =
|
|
27
|
+
aWeights.x * uJointMatrices[int(aJoints.x)] +
|
|
28
|
+
aWeights.y * uJointMatrices[int(aJoints.y)] +
|
|
29
|
+
aWeights.z * uJointMatrices[int(aJoints.z)] +
|
|
30
|
+
aWeights.w * uJointMatrices[int(aJoints.w)];
|
|
31
|
+
|
|
32
|
+
// 2. Skinned local position
|
|
33
|
+
vec4 localPos = skinMatrix * aVertexPosition;
|
|
34
|
+
|
|
35
|
+
// 3. Final projection
|
|
36
|
+
gl_Position = uProjectionMatrix * uModelViewMatrix * localPos;
|
|
37
|
+
|
|
38
|
+
vTextureCoord = (aTextureCoord * uTexScale) + uTexOffset;
|
|
39
|
+
|
|
40
|
+
// Compute world position for lighting calculations
|
|
41
|
+
vWorldPos = (uModelMatrix * localPos).xyz;
|
|
42
|
+
|
|
43
|
+
// Approximate normal transformation using ModelMatrix directly
|
|
44
|
+
// This is valid for Translation/Rotation uniform scaling
|
|
45
|
+
mat3 normalSkinMatrix = mat3(skinMatrix);
|
|
46
|
+
vNormal = normalize(mat3(uModelMatrix) * normalSkinMatrix * aVertexNormal);
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
export class SkinnedLitMaterial extends Material {
|
|
51
|
+
constructor() {
|
|
52
|
+
// Uses the exact same Fragment Shader from LitMaterial for a unified lighting pipeline
|
|
53
|
+
super(SKINNED_LIT_VS, LIT_SPRITE_FS);
|
|
54
|
+
}
|
|
55
|
+
}
|