diff --git a/assets/sky/night_sky.jpg b/assets/sky/night_sky.jpg new file mode 100644 index 0000000..5409634 Binary files /dev/null and b/assets/sky/night_sky.jpg differ diff --git a/assets/sky/night_sky.png b/assets/sky/night_sky.png new file mode 100644 index 0000000..c8e11d8 Binary files /dev/null and b/assets/sky/night_sky.png differ diff --git a/graphics.md b/graphics.md new file mode 100644 index 0000000..f753223 --- /dev/null +++ b/graphics.md @@ -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 ~30–40% 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. diff --git a/src/car.odin b/src/car.odin index 078867c..7169693 100644 --- a/src/car.odin +++ b/src/car.odin @@ -14,16 +14,19 @@ Car :: struct { speed: f32, forward: rl.Vector3, wheel_yaw: f32, + steer: f32, + yaw_rate: f32, } EngineState :: struct { - throttle: f32, - brake: f32, - rpm: f32, - torque: f32, - fuel_cut: bool, - engine: Engine, - graph: Graph, + throttle: f32, + brake: f32, + handbrake: f32, + rpm: f32, + torque: f32, + fuel_cut: bool, + engine: Engine, + graph: Graph, } MIN_RPM :: 1000 @@ -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 { - car.vel = rl.Vector3Normalize(car.vel) - } + // 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 } diff --git a/src/game.odin b/src/game.odin index e83ee9a..0d82613 100644 --- a/src/game.odin +++ b/src/game.odin @@ -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) } diff --git a/src/graphics.odin b/src/graphics.odin index 9c66c2f..839f3bb 100644 --- a/src/graphics.odin +++ b/src/graphics.odin @@ -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, + 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) } diff --git a/src/main.odin b/src/main.odin index a5306a3..0abd72c 100644 --- a/src/main.odin +++ b/src/main.odin @@ -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() diff --git a/src/shader/skybox.fs b/src/shader/skybox.fs new file mode 100644 index 0000000..678a9e3 --- /dev/null +++ b/src/shader/skybox.fs @@ -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); +} diff --git a/src/shader/skybox.vs b/src/shader/skybox.vs new file mode 100644 index 0000000..ad10357 --- /dev/null +++ b/src/shader/skybox.vs @@ -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); +} diff --git a/src/shader/wet_road.fs b/src/shader/wet_road.fs new file mode 100644 index 0000000..976de64 --- /dev/null +++ b/src/shader/wet_road.fs @@ -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); +}