vibecoded graphics
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
+73
@@ -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.
|
||||
+112
-25
@@ -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
|
||||
}
|
||||
|
||||
+5
-2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+98
-4
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user