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.
|
||||||
+104
-17
@@ -14,11 +14,14 @@ Car :: struct {
|
|||||||
speed: f32,
|
speed: f32,
|
||||||
forward: rl.Vector3,
|
forward: rl.Vector3,
|
||||||
wheel_yaw: f32,
|
wheel_yaw: f32,
|
||||||
|
steer: f32,
|
||||||
|
yaw_rate: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineState :: struct {
|
EngineState :: struct {
|
||||||
throttle: f32,
|
throttle: f32,
|
||||||
brake: f32,
|
brake: f32,
|
||||||
|
handbrake: f32,
|
||||||
rpm: f32,
|
rpm: f32,
|
||||||
torque: f32,
|
torque: f32,
|
||||||
fuel_cut: bool,
|
fuel_cut: bool,
|
||||||
@@ -46,6 +49,17 @@ ROLL_FORCE :: 176
|
|||||||
CLUTCH_K :: 0.15
|
CLUTCH_K :: 0.15
|
||||||
CLUTCH_MAX :: 300 // предел трения сцепления, N·m
|
CLUTCH_MAX :: 300 // предел трения сцепления, N·m
|
||||||
BRAKE_MAX :: 10500 // ~0.9g для 1200 кг
|
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 {
|
Drivetrain :: struct {
|
||||||
ratios: [5]f32,
|
ratios: [5]f32,
|
||||||
@@ -87,36 +101,93 @@ update_car :: proc(car: ^Car) {
|
|||||||
update_turning :: proc(car: ^Car) {
|
update_turning :: proc(car: ^Car) {
|
||||||
dt := rl.GetFrameTime()
|
dt := rl.GetFrameTime()
|
||||||
|
|
||||||
// 1. Get input direction (-1 for left, 1 for right)
|
// 1. Steering input (-1 вправо, +1 влево) -> сглаженный угол руля
|
||||||
input: f32 = 0
|
input: f32 = 0
|
||||||
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) {input -= 1}
|
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) {input -= 1}
|
||||||
if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) {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
|
// 2. Текущий угол увода: направление движения vs курс кузова.
|
||||||
speed_scale := math.clamp(car.speed / 200.0, 0.0, 1.0)
|
// Боковая сила шины растёт с углом до пика SLIP_PEAK и падает после —
|
||||||
turn_speed := f32(6.5) // Adjust turn speed in radians per second
|
// это «край сцепления», за ним машина едет боком.
|
||||||
|
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)
|
// 3. Желаемый yaw rate: геометрия Ackermann на малой скорости,
|
||||||
car.wheel_yaw += input * turn_speed * speed_scale * dt
|
// на скорости ограничен сцеплением: |ω| <= 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
|
// 4. Реакция на руль с инерцией (первый порядок), интегрируем курс
|
||||||
// (X = sin, Z = cos gives standard 3D forward direction where 0 rad points North/Forward)
|
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 {
|
car.forward = rl.Vector3 {
|
||||||
math.sin(car.wheel_yaw),
|
math.sin(car.wheel_yaw),
|
||||||
0,
|
0,
|
||||||
math.cos(car.wheel_yaw),
|
math.cos(car.wheel_yaw),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Arcade Grip / Blend Velocity towards Forward Vector
|
// 5. Скольжение: скорость раскладываем вдоль осей кузова,
|
||||||
// Lower grip value (e.g. 5.0) = loose / drifting turn
|
// боковая компонента гасится сцеплением (до LAT_A_MAX * track_grip).
|
||||||
// Higher grip value (e.g. 20.0) = tight / glued to road
|
// В заносе это само собой съедает скорость (scrub).
|
||||||
grip_factor: f32 = 5.0
|
vf := car.speed * math.cos(theta)
|
||||||
car.vel = linalg.lerp(car.vel, car.forward, grip_factor * dt)
|
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
|
// 6. Поворачиваем направление движения на изменение угла заноса
|
||||||
if rl.Vector3Length(car.vel) > 0 {
|
car.vel = rotate_y(car.vel, theta_new - theta)
|
||||||
car.vel = rl.Vector3Normalize(car.vel)
|
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) {
|
update_gear :: proc(drivetrain: ^Drivetrain, speed: f32) {
|
||||||
@@ -151,6 +222,19 @@ update_engine_state :: proc(car: ^Car) {
|
|||||||
car.engine_state.brake = 0
|
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) {
|
if rl.IsKeyDown(.W) {
|
||||||
car.engine_state.throttle = linalg.lerp(
|
car.engine_state.throttle = linalg.lerp(
|
||||||
car.engine_state.throttle,
|
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 +=
|
car.speed +=
|
||||||
(force -
|
(force -
|
||||||
DRAG_COEF * car.speed * car.speed -
|
DRAG_COEF * car.speed * car.speed -
|
||||||
@@ -303,6 +389,7 @@ init_car :: proc() -> Car {
|
|||||||
}
|
}
|
||||||
|
|
||||||
car.forward = rl.Vector3{0, 0, 1}
|
car.forward = rl.Vector3{0, 0, 1}
|
||||||
|
car.vel = car.forward
|
||||||
|
|
||||||
return car
|
return car
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -37,6 +37,8 @@ init_game_state :: proc() -> GameState {
|
|||||||
cleanup_game_state :: proc(state: ^GameState) {
|
cleanup_game_state :: proc(state: ^GameState) {
|
||||||
gfx_unload(&state.gfx_state)
|
gfx_unload(&state.gfx_state)
|
||||||
engine_sound_unload(&state.sound)
|
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) {
|
update_game_state :: proc(state: ^GameState) {
|
||||||
@@ -60,7 +62,7 @@ update_game_state :: proc(state: ^GameState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draw_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)
|
rl.BeginMode3D(state.camera)
|
||||||
{
|
{
|
||||||
draw_3d(state)
|
draw_3d(state)
|
||||||
@@ -73,7 +75,8 @@ draw_game_state :: proc(state: ^GameState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draw_3d :: 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)
|
gfx_draw_car(&state.gfx_state, state.car.pos, state.car.wheel_yaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+95
-1
@@ -2,11 +2,20 @@ package main
|
|||||||
|
|
||||||
import "core:math"
|
import "core:math"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
import rlgl "vendor:raylib/rlgl"
|
||||||
|
|
||||||
GfxState :: struct {
|
GfxState :: struct {
|
||||||
shader: rl.Shader,
|
shader: rl.Shader,
|
||||||
shader_loc: i32,
|
shader_loc: i32,
|
||||||
car: rl.Model,
|
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 {
|
gfx_init :: proc() -> GfxState {
|
||||||
@@ -39,12 +48,82 @@ gfx_init :: proc() -> GfxState {
|
|||||||
state.car.materials[i].shader = state.shader
|
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
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx_unload :: proc(state: ^GfxState) {
|
gfx_unload :: proc(state: ^GfxState) {
|
||||||
|
// NOTE: UnloadMaterial() сам освобождает шейдер материала — не звать UnloadShader повторно!
|
||||||
rl.UnloadModel(state.car)
|
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) {
|
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)
|
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) {
|
gfx_update :: proc(state: ^GfxState, cam_pos: ^rl.Vector3) {
|
||||||
rl.SetShaderValue(state.shader, state.shader_loc, cam_pos, .VEC3)
|
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"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
|
rl.SetConfigFlags({.MSAA_4X_HINT}) // сглаживание тонких бликов и краёв
|
||||||
rl.InitWindow(1280, 720, "Odin + Raylib - NFS Style Lighting")
|
rl.InitWindow(1280, 720, "Odin + Raylib - NFS Style Lighting")
|
||||||
defer rl.CloseWindow()
|
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