vibecoded graphics

This commit is contained in:
2026-08-16 23:04:02 +05:00
parent 41ec89a732
commit c7a862c058
10 changed files with 449 additions and 31 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

+73
View File
@@ -0,0 +1,73 @@
# BRIEF: NFS 2015-Style Wet Perma-Night Scene Setup (Raylib / C)
## 1. Goal
Transform an existing basic 3D scene (containing car model, road surfaces, and city building meshes with flat albedo textures) into a high-contrast, moody, wet perma-night aesthetic inspired by *Need for Speed (2015)*.
---
## 2. Technical Stack Context
- **Framework:** Raylib (C/C++)
- **Core APIs:** Custom GLSL Shaders (Shader / Fragment Shaders), `RenderTexture2D` (FBOs), `rlgl` for state/texture management.
- **Goal Output:** Modular shader code and pass-management architecture that runs efficiently on Raylib's forward renderer.
---
## 3. Implementation Roadmap & Execution Tasks
### Phase 1: Material & Wetness Shader (`wet_road.fs`)
Modify the primary object shader used for terrain/road surfaces to support a wetness model.
- [ ] **Inputs Needed:**
- `texture0` (Albedo / Diffuse)
- `texture1` (Normal Map)
- `texture2` (Roughness Map or Puddle Noise Mask)
- Uniform `float u_wetness` (Range: `0.0` to `1.0`)
- [ ] **Shader Logic Requirements:**
- **Albedo Darkening:** Darken base albedo by ~3040% on wet areas (porous surfaces darken when saturated).
- **Roughness Modulation:** Force surface roughness down to near-mirror levels (`~0.02 - 0.1`) based on `u_wetness` and the puddle mask.
- **Normal Flattening for Puddles:** In high-puddle mask areas, lerp surface normals toward `vec3(0.0, 1.0, 0.0)` to simulate smooth standing water.
### Phase 2: Lighting & Emissive Setup
Configure scene lights and emissive materials for night contrast.
- [ ] **Global Ambient Light:** Lower global ambient color to a dark, cool navy (`RGB: 10, 15, 30`).
- [ ] **Emissive Material Handling:**
- Add support for an emissive map/uniform on neon signs, building windows, and car light meshes.
- Multiply emissive output by an intensity factor (`> 2.0`) to ensure they trigger the Bloom threshold in post-processing.
- [ ] **Point & Spot Lights:**
- Implement dynamic point lights for streetlights and headlights with quadratic distance attenuation (`1.0 / (att_const + att_linear * d + att_quad * d * d)`).
### Phase 3: Post-Processing Framebuffer Pipeline (`main.c`)
Set up offscreen rendering to enable screen-space passes.
- [ ] **Render Texture Setup:**
- Create a master `RenderTexture2D targetScene` FBO matching screen resolution.
- Create a secondary `RenderTexture2D pingPongBlur[2]` FBO pair for downscaled bloom processing.
- [ ] **Render Loop Flow:**
1. `BeginTextureMode(targetScene)` $\rightarrow$ Render all 3D geometry using object shaders.
2. `EndTextureMode()`
3. Extract bright pixels from `targetScene` to bloom FBO via threshold shader (`luminance > 0.7`).
4. Perform horizontal and vertical blur passes on the bright map.
5. Composite original scene + blurred bloom + final color grading onto the screen.
### Phase 4: Color Grading & Anamorphic Bloom Shader (`post_process.fs`)
Apply the final cinematic "lens" pass over the scene texture.
- [ ] **Heavy / Anamorphic Bloom:**
- Scale horizontal blur kernel radius larger than vertical to simulate anamorphic lens flares from streetlights and wet ground reflections.
- [ ] **High-Contrast Night LUT / Grading:**
- Crushed deep blacks (`#050508`).
- Cool cyan/teal shadows and warm orange/amber highlights for light sources.
- [ ] **Vignette & Lens Effects:**
- Radial vignette intensity dropping at screen edges.
- Subtle chromatic aberration (offset Red/Blue channels slightly near screen borders).
### Phase 5: Simple Reflection Pass (Ground / Car)
- [ ] Implement a basic planar reflection pass or sample a low-resolution cubemap probe inside the `wet_road.fs` fragment shader to give wet surfaces mirror-like reflections of the ambient sky and light sources.
---
## 4. Deliverables Checklist
1. `wet_material.fs` / `wet_material.vs`: Shader handling albedo darkening, normal mapping, and specular puddle blending.
2. `post_process.fs`: Screen-space shader combining Bloom, Color Grading, Vignette, and Chromatic Aberration.
3. Updated Raylib C loop establishing the `RenderTexture2D` pipeline and uniform bindings.
+103 -16
View File
@@ -14,11 +14,14 @@ Car :: struct {
speed: f32,
forward: rl.Vector3,
wheel_yaw: f32,
steer: f32,
yaw_rate: f32,
}
EngineState :: struct {
throttle: f32,
brake: f32,
handbrake: f32,
rpm: f32,
torque: f32,
fuel_cut: bool,
@@ -46,6 +49,17 @@ ROLL_FORCE :: 176
CLUTCH_K :: 0.15
CLUTCH_MAX :: 300 // предел трения сцепления, N·m
BRAKE_MAX :: 10500 // ~0.9g для 1200 кг
HANDBRAKE_MAX :: 13000 // ручной тормоз (пробел), N — ~1.1g
// --- Поворот: аркадная bicycle-модель ---
MAX_STEER_ANGLE :: 0.6 // полный угол руля, рад (~34°)
STEER_RATE :: 4.0 // рад/с — скорость доворота руля до упора
WHEELBASE :: 2.6 // м — колёсная база
LAT_A_MAX :: 12.0 // м/с² — предел бокового ускорения (~1.2g)
YAW_RESPONSE :: 10.0 // 1/с — инерция реакции на руль (лаг yaw)
SLIP_PEAK :: 0.25 // рад (~14°) — угол увода на пике боковой силы шины
SLIP_MAX :: 0.80 // рад (~46°) — сцепление падает до нуля
MIN_TURN_SPEED :: 0.5 // м/с — защита от деления на ноль
Drivetrain :: struct {
ratios: [5]f32,
@@ -87,36 +101,93 @@ update_car :: proc(car: ^Car) {
update_turning :: proc(car: ^Car) {
dt := rl.GetFrameTime()
// 1. Get input direction (-1 for left, 1 for right)
// 1. Steering input (-1 вправо, +1 влево) -> сглаженный угол руля
input: f32 = 0
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) {input -= 1}
if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) {input += 1}
car.steer = approach(car.steer, input * MAX_STEER_ANGLE, STEER_RATE * dt)
// 2. Turn sensitivity scaling based on speed
speed_scale := math.clamp(car.speed / 200.0, 0.0, 1.0)
turn_speed := f32(6.5) // Adjust turn speed in radians per second
// 2. Текущий угол увода: направление движения vs курс кузова.
// Боковая сила шины растёт с углом до пика SLIP_PEAK и падает после —
// это «край сцепления», за ним машина едет боком.
theta := slip_angle(car.vel, car.forward)
track_grip := tracking_grip(theta) // сцепление для пути
// ручной тормоз срывает заднюю ось: сцепление пути резко падает -> занос
track_grip *= 1 - 0.7 * car.engine_state.handbrake
yaw_author := yaw_authority(theta) // авторитет курса (в спаде падает быстрее)
// 3. Update the car's orientation angle (yaw)
car.wheel_yaw += input * turn_speed * speed_scale * dt
// 3. Желаемый yaw rate: геометрия Ackermann на малой скорости,
// на скорости ограничен сцеплением: |ω| <= LAT_A_MAX * yaw_author / v.
// Радиус на скорости растёт как v^2 — быстрый вираж требует торможения.
v := max(car.speed, MIN_TURN_SPEED) // пол только для деления (защита от /0)
yaw_des := car.speed * math.tan(car.steer) / WHEELBASE
yaw_max := LAT_A_MAX * yaw_author / v
yaw_des = math.clamp(yaw_des, -yaw_max, yaw_max)
// 4. Calculate forward vector directly from yaw
// (X = sin, Z = cos gives standard 3D forward direction where 0 rad points North/Forward)
// 4. Реакция на руль с инерцией (первый порядок), интегрируем курс
response := 1 - math.exp(-YAW_RESPONSE * dt)
car.yaw_rate += (yaw_des - car.yaw_rate) * response
car.wheel_yaw += car.yaw_rate * dt
car.forward = rl.Vector3 {
math.sin(car.wheel_yaw),
0,
math.cos(car.wheel_yaw),
}
// 5. Arcade Grip / Blend Velocity towards Forward Vector
// Lower grip value (e.g. 5.0) = loose / drifting turn
// Higher grip value (e.g. 20.0) = tight / glued to road
grip_factor: f32 = 5.0
car.vel = linalg.lerp(car.vel, car.forward, grip_factor * dt)
// 5. Скольжение: скорость раскладываем вдоль осей кузова,
// боковая компонента гасится сцеплением (до LAT_A_MAX * track_grip).
// В заносе это само собой съедает скорость (scrub).
vf := car.speed * math.cos(theta)
vl := car.speed * math.sin(theta)
vl = approach(vl, 0, LAT_A_MAX * track_grip * dt)
car.speed = math.sqrt(vf * vf + vl * vl)
theta_new := math.atan2(vl, vf)
// Normalize velocity vector to prevent unintended scaling
if rl.Vector3Length(car.vel) > 0 {
// 6. Поворачиваем направление движения на изменение угла заноса
car.vel = rotate_y(car.vel, theta_new - theta)
car.vel = rl.Vector3Normalize(car.vel)
}
// approach — движение cur к target не быстрее max_delta за шаг
approach :: proc(cur, target, max_delta: f32) -> f32 {
if max_delta < 0 do return target
if cur < target do return min(cur + max_delta, target)
return max(cur - max_delta, target)
}
// slip_angle — знаковый угол (в XZ) между направлением движения и курсом
slip_angle :: proc(vel, forward: rl.Vector3) -> f32 {
s := vel.x * forward.z - vel.z * forward.x
c := vel.x * forward.x + vel.z * forward.z
return math.atan2(s, c)
}
// rotate_y — поворот вектора вокруг Y (right-handed, согласован с yaw)
rotate_y :: proc(v: rl.Vector3, angle: f32) -> rl.Vector3 {
c := math.cos(angle)
s := math.sin(angle)
return rl.Vector3{c * v.x + s * v.z, v.y, -s * v.x + c * v.z}
}
// tracking_grip — сцепление для пути (боковой трекинг скорости):
// растёт с углом увода до пика, потом квадратичный спад до нуля.
tracking_grip :: proc(theta: f32) -> f32 {
a := math.abs(theta)
if a >= SLIP_MAX do return 0
if a <= SLIP_PEAK do return a / SLIP_PEAK
t := math.clamp((a - SLIP_PEAK) / (SLIP_MAX - SLIP_PEAK), 0, 1)
return 1 - t * t
}
// yaw_authority — авторитет курса: на малом уводе полный (передние колёса держат),
// в зоне спада падает быстрее, чем tracking_grip (h = g²) — занос сам себя
// ограничивает и возвращается к пику, а не раскручивается в штопор.
yaw_authority :: proc(theta: f32) -> f32 {
a := math.abs(theta)
if a <= SLIP_PEAK do return 1
t := math.clamp((a - SLIP_PEAK) / (SLIP_MAX - SLIP_PEAK), 0, 1)
g := 1 - t * t
return g * g
}
update_gear :: proc(drivetrain: ^Drivetrain, speed: f32) {
@@ -151,6 +222,19 @@ update_engine_state :: proc(car: ^Car) {
car.engine_state.brake = 0
}
}
if rl.IsKeyDown(.SPACE) {
car.engine_state.handbrake = linalg.lerp(car.engine_state.handbrake, 1, 0.2)
} else {
if car.engine_state.handbrake >= 0.1 {
car.engine_state.handbrake = linalg.lerp(
car.engine_state.handbrake,
0,
0.2,
)
} else {
car.engine_state.handbrake = 0
}
}
if rl.IsKeyDown(.W) {
car.engine_state.throttle = linalg.lerp(
car.engine_state.throttle,
@@ -233,7 +317,9 @@ update_engine_state :: proc(car: ^Car) {
}
// скорость — всегда, с тормозом (и на нейтрали тоже)
brake_force := car.engine_state.brake * BRAKE_MAX
brake_force :=
car.engine_state.brake * BRAKE_MAX +
car.engine_state.handbrake * HANDBRAKE_MAX
car.speed +=
(force -
DRAG_COEF * car.speed * car.speed -
@@ -303,6 +389,7 @@ init_car :: proc() -> Car {
}
car.forward = rl.Vector3{0, 0, 1}
car.vel = car.forward
return car
}
+5 -2
View File
@@ -37,6 +37,8 @@ init_game_state :: proc() -> GameState {
cleanup_game_state :: proc(state: ^GameState) {
gfx_unload(&state.gfx_state)
engine_sound_unload(&state.sound)
delete(state.car.engine_state.graph.points)
delete(state.car.engine_state.engine.rpm_lut)
}
update_game_state :: proc(state: ^GameState) {
@@ -60,7 +62,7 @@ update_game_state :: proc(state: ^GameState) {
}
draw_game_state :: proc(state: ^GameState) {
rl.ClearBackground(rl.GetColor(0x181825FF)) // Dark background
rl.ClearBackground(rl.GetColor(0x080A12FF)) // почти чёрная ночь — дорога должна отделяться от фона
rl.BeginMode3D(state.camera)
{
draw_3d(state)
@@ -73,7 +75,8 @@ draw_game_state :: proc(state: ^GameState) {
}
draw_3d :: proc(state: ^GameState) {
rl.DrawGrid(200, 2.0)
gfx_draw_skybox(&state.gfx_state, state.camera.position)
gfx_draw_road(&state.gfx_state)
gfx_draw_car(&state.gfx_state, state.car.pos, state.car.wheel_yaw)
}
+95 -1
View File
@@ -2,11 +2,20 @@ package main
import "core:math"
import rl "vendor:raylib"
import rlgl "vendor:raylib/rlgl"
GfxState :: struct {
shader: rl.Shader,
shader_loc: i32,
car: rl.Model,
road_mesh: rl.Mesh,
road_mat: rl.Material,
road_shader: rl.Shader,
road_view_loc: i32,
skybox_mesh: rl.Mesh,
skybox_mat: rl.Material,
skybox_shader: rl.Shader,
skybox_tex: rl.Texture2D,
}
gfx_init :: proc() -> GfxState {
@@ -39,12 +48,82 @@ gfx_init :: proc() -> GfxState {
state.car.materials[i].shader = state.shader
}
// --- Мокрая дорога: процедурная плоскость (вершинный шейдер переиспользуется) ---
state.road_shader = rl.LoadShader("src/shader/nfs_car.vs", "src/shader/wet_road.fs")
road_light_dir_loc := rl.GetShaderLocation(state.road_shader, "lightDir")
road_light_col_loc := rl.GetShaderLocation(state.road_shader, "lightColor")
road_ambient_loc := rl.GetShaderLocation(state.road_shader, "ambientColor")
state.road_view_loc = rl.GetShaderLocation(state.road_shader, "viewPos")
albedo_loc := rl.GetShaderLocation(state.road_shader, "u_albedo")
wet_loc := rl.GetShaderLocation(state.road_shader, "u_wetness")
scale_loc := rl.GetShaderLocation(state.road_shader, "u_noiseScale")
detail_loc := rl.GetShaderLocation(state.road_shader, "u_detailScale")
size_loc := rl.GetShaderLocation(state.road_shader, "u_planeSize")
// холодный лунный свет: луна высоко ВПЕРЕДИ (45°) — дорога хорошо освещена.
// Лунная дорожка в шейдере отдельно, как отражение неба (не зависит от диффуза).
road_light_dir := rl.Vector3{0, -0.707, -0.707}
road_light_col := rl.Vector3{1.1, 1.15, 1.3}
road_ambient := rl.Vector3{0.15, 0.16, 0.22}
rl.SetShaderValue(state.road_shader, road_light_dir_loc, &road_light_dir, .VEC3)
rl.SetShaderValue(state.road_shader, road_light_col_loc, &road_light_col, .VEC3)
rl.SetShaderValue(state.road_shader, road_ambient_loc, &road_ambient, .VEC3)
asphalt := rl.Vector3{0.24, 0.25, 0.28}
wetness := f32(0.9)
noise_scale := f32(50.0) // 50 ячеек/UV = ~4 м — крупная маска луж
detail_scale := f32(300.0) // 300 ячеек/UV = ~0.7 м — тайлинг фактуры асфальта
plane_size := f32(200.0)
rl.SetShaderValue(state.road_shader, albedo_loc, &asphalt, .VEC3)
rl.SetShaderValue(state.road_shader, wet_loc, &wetness, .FLOAT)
rl.SetShaderValue(state.road_shader, scale_loc, &noise_scale, .FLOAT)
rl.SetShaderValue(state.road_shader, detail_loc, &detail_scale, .FLOAT)
rl.SetShaderValue(state.road_shader, size_loc, &plane_size, .FLOAT)
// видимая луна: низко по курсу (~8.6°) — в кадре; дорожка на дороге отражает её
moon_dir := rl.Vector3{0, 0.15, 0.988}
moon_col := rl.Vector3{1.0, 1.05, 1.2}
rl.SetShaderValue(state.road_shader, rl.GetShaderLocation(state.road_shader, "u_moonDir"), &moon_dir, .VEC3)
rl.SetShaderValue(state.road_shader, rl.GetShaderLocation(state.road_shader, "u_moonColor"), &moon_col, .VEC3)
state.road_mesh = rl.GenMeshPlane(plane_size, plane_size, 64, 64)
rl.UploadMesh(&state.road_mesh, false) // raylib 6.0: LoadModelFromMesh не аплоадит сам
state.road_mat = rl.LoadMaterialDefault()
state.road_mat.shader = state.road_shader
// --- Скайбокс: equirectangular ночное небо + процедурная луна ---
state.skybox_shader = rl.LoadShader("src/shader/skybox.vs", "src/shader/skybox.fs")
sky_img := rl.LoadImage("assets/sky/night_sky.png") // PNG: raylib 6.0 не декодирует JPG
defer rl.UnloadImage(sky_img)
state.skybox_tex = rl.LoadTextureFromImage(sky_img)
// видимая луна: низко по курсу (~8.6°) — совпадает с дорожкой на дороге
sky_mul := f32(0.65) // затемнение картинки: тёмное ночное небо, но читаемое
rl.SetShaderValue(state.skybox_shader, rl.GetShaderLocation(state.skybox_shader, "moonDir"), &moon_dir, .VEC3)
rl.SetShaderValue(state.skybox_shader, rl.GetShaderLocation(state.skybox_shader, "moonColor"), &moon_col, .VEC3)
rl.SetShaderValue(state.skybox_shader, rl.GetShaderLocation(state.skybox_shader, "u_skyMul"), &sky_mul, .FLOAT)
state.skybox_mesh = rl.GenMeshCube(1, 1, 1)
rl.UploadMesh(&state.skybox_mesh, false)
state.skybox_mat = rl.LoadMaterialDefault()
state.skybox_mat.shader = state.skybox_shader
state.skybox_mat.maps[rl.MaterialMapIndex.ALBEDO].texture = state.skybox_tex // texture0 в skybox.fs
return state
}
gfx_unload :: proc(state: ^GfxState) {
// NOTE: UnloadMaterial() сам освобождает шейдер материала — не звать UnloadShader повторно!
rl.UnloadModel(state.car)
rl.UnloadShader(state.shader)
rl.UnloadShader(state.shader) // шейдер машины — UnloadModel материалы не освобождает
rl.UnloadMesh(state.road_mesh)
rl.UnloadMaterial(state.road_mat) // освобождает state.road_shader
rl.UnloadMesh(state.skybox_mesh)
rl.UnloadMaterial(state.skybox_mat) // освобождает state.skybox_shader
rl.UnloadTexture(state.skybox_tex)
}
gfx_draw_car :: proc(state: ^GfxState, pos: rl.Vector3, yaw: f32) {
@@ -53,6 +132,21 @@ gfx_draw_car :: proc(state: ^GfxState, pos: rl.Vector3, yaw: f32) {
rl.DrawModelEx(state.car, pos, rotation_axis, angle_degrees, 1.0, rl.WHITE)
}
gfx_draw_road :: proc(state: ^GfxState) {
// DrawMesh напрямую: в raylib 6.0 LoadModelFromMesh не аплоадит меш в GPU
rl.DrawMesh(state.road_mesh, state.road_mat, rl.Matrix(1))
}
gfx_draw_skybox :: proc(state: ^GfxState, cam_pos: rl.Vector3) {
// куб вокруг камеры: translate(camera) * scale(500) — рисуется первым, дорога его перекрывает.
// Backface culling выключен: камера внутри куба смотрит на внутренние грани.
transform := rl.MatrixTranslate(cam_pos.x, cam_pos.y, cam_pos.z) * rl.MatrixScale(500, 500, 500)
rlgl.DisableBackfaceCulling()
rl.DrawMesh(state.skybox_mesh, state.skybox_mat, transform)
rlgl.EnableBackfaceCulling()
}
gfx_update :: proc(state: ^GfxState, cam_pos: ^rl.Vector3) {
rl.SetShaderValue(state.shader, state.shader_loc, cam_pos, .VEC3)
rl.SetShaderValue(state.road_shader, state.road_view_loc, cam_pos, .VEC3)
}
+1
View File
@@ -5,6 +5,7 @@ import math "core:math"
import rl "vendor:raylib"
main :: proc() {
rl.SetConfigFlags({.MSAA_4X_HINT}) // сглаживание тонких бликов и краёв
rl.InitWindow(1280, 720, "Odin + Raylib - NFS Style Lighting")
defer rl.CloseWindow()
+37
View File
@@ -0,0 +1,37 @@
#version 330
// Скайбокс: equirectangular ночное небо (texture0) + процедурная луна.
// Направление на фрагмент -> UV equirect -> сэмпл картинки.
// Луна рисуется поверх в направлении moonDir (совпадает с освещением дороги).
in vec3 fragDir;
out vec4 finalColor;
uniform sampler2D texture0; // equirectangular небо (2:1)
uniform vec3 moonDir; // направление НА луну (world space)
uniform vec3 moonColor;
uniform float u_skyMul; // затемнение неба под ночной муд
void main() {
vec3 d = normalize(fragDir);
vec3 moonD = normalize(moonDir);
// equirect: u по азимуту, v по высоте (v=0 — зенит = верх картинки)
vec2 uv = vec2(
atan(d.z, d.x) / 6.2831853 + 0.5,
acos(clamp(d.y, -1.0, 1.0)) / 3.1415927
);
vec3 sky = texture(texture0, uv).rgb * u_skyMul;
float md = dot(d, moonD);
// диск луны (~1°) с мягким краем
float disc = smoothstep(0.9996, 0.9998, md);
// гало: широкое + узкое
float glow = pow(max(md, 0.0), 32.0) * 0.12 + pow(max(md, 0.0), 256.0) * 0.5;
vec3 moon = moonColor * (disc * 1.6 + glow);
finalColor = vec4(sky + moon, 1.0);
}
+12
View File
@@ -0,0 +1,12 @@
#version 330
in vec3 vertexPosition;
out vec3 fragDir;
uniform mat4 mvp;
void main() {
fragDir = vertexPosition;
gl_Position = mvp * vec4(vertexPosition, 1.0);
}
+111
View File
@@ -0,0 +1,111 @@
#version 330
// Мокрая дорога — полностью процедурная (без текстур):
// value-noise + fbm -> маска луж, рельефная нормаль, затемнение асфальта,
// блик от лунного света. Вершинный шейдер переиспользуется nfs_car.vs.
in vec2 fragTexCoord;
in vec3 fragNormal;
in vec3 fragFragPos;
in vec4 fragColor;
out vec4 finalColor;
uniform vec4 colDiffuse;
// Общий свет (те же юниформы, что у машины)
uniform vec3 lightDir;
uniform vec3 lightColor;
uniform vec3 ambientColor;
uniform vec3 viewPos;
// Мокрая дорога
uniform vec3 u_albedo; // базовый цвет асфальта
uniform float u_wetness; // 0..1 — глобальная мокрость
uniform float u_noiseScale; // масштаб МАСКИ ЛУЖ (крупный)
uniform float u_detailScale; // тайлинг мелкой фактуры асфальта (моттл + нормали)
uniform float u_planeSize; // размер плоскости в мире (для градиента нормали)
uniform vec3 u_moonDir; // направление НА видимую луну (world)
uniform vec3 u_moonColor; // цвет луны (для дорожки)
// --- процедурный value-noise (без текстур) ---
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2(1, 0)), u.x),
mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x),
u.y);
}
float fbm(vec2 p) {
return 0.5 * noise(p) + 0.25 * noise(p * 2.0 + 17.0) + 0.125 * noise(p * 4.0 + 43.0);
}
// мелкий рельеф дороги (для нормали) — тайлинг фактуры асфальта
float height(vec2 uv) {
return fbm(uv * u_detailScale);
}
// маска луж: высокие области fbm, порог + контраст -> чёткие пятна
float puddle_mask(vec2 uv) {
vec2 p = uv * u_noiseScale * 0.25;
float m = smoothstep(0.50, 0.68, fbm(p));
return m * m; // выталкивает к 0/1: сухо или лужа, без полутонов
}
// нормаль из градиента шума (конечные разности, масштаб в мировых единицах)
// рельеф едва заметный: асфальт должен быть ровным, а не «серым морем»
vec3 road_normal(vec2 uv) {
float e = 0.0015; // шаг в UV
float h = height(uv);
float hx = height(uv + vec2(e, 0.0));
float hz = height(uv + vec2(0.0, e));
vec2 slope = vec2(hx - h, hz - h) / (e * u_planeSize);
return normalize(vec3(-slope.x * 0.3, 1.0, -slope.y * 0.3));
}
void main() {
vec2 uv = fragTexCoord;
float mask = puddle_mask(uv);
// сухой асфальт — полностью матовый, лужи — до u_wetness
float wet = u_wetness * mask;
// мелкая фактура асфальта — едва заметное зерно, без волн
float mottle = mix(0.985, 1.015, fbm(uv * u_detailScale + 7.3));
vec3 albedo = u_albedo * mottle * (1.0 - 0.7 * wet);
// нормаль: мелкий рельеф + сглаживание в лужах (гладкая вода)
vec3 N = normalize(mix(road_normal(uv), vec3(0.0, 1.0, 0.0), wet * wet));
vec3 L = normalize(-lightDir);
vec3 V = normalize(viewPos - fragFragPos);
vec3 H = normalize(L + V);
float diff = max(dot(N, L), 0.0);
vec3 diffuse = diff * lightColor;
// искры на фасетах (слабые — дорожку даёт отражение неба ниже)
float shininess = mix(24.0, 64.0, wet);
float spec = pow(max(dot(N, H), 0.0), shininess) * mix(0.05, 0.5, wet);
// ЛУННАЯ ДОРОЖКА: мокрая дорога отражает ВИДИМУЮ луну (u_moonDir, низко по курсу).
// R — куда смотрит отражение; ярко там, где оно попадает в диск/гало луны.
// Дорожка непрерывная (тонкая плёнка на всей дороге), в лужах — ярче.
vec3 R = reflect(-V, N);
vec3 moonD = normalize(u_moonDir);
float moonHit = pow(max(dot(R, moonD), 0.0), 32.0)
+ pow(max(dot(R, moonD), 0.0), 8.0) * 0.2; // узкое гало
vec3 moonPath = moonHit * u_moonColor * mix(0.06, 1.0, wet);
// база (лужи темнее) + отражения ПОВЕРХ неё только где есть реальный свет
vec3 base = albedo * (ambientColor + diffuse);
vec3 refl = moonPath + lightColor * spec;
finalColor = vec4(base + refl, 1.0);
}