Created
October 8, 2025 00:01
-
-
Save celsowm/b62ce09d3c8ddcad1874b1dda304149e to your computer and use it in GitHub Desktop.
PlatformTriplanar.gd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| extends Node3D | |
| # Bloco flare + tampa com bordinha picotada (soldada ao topo) | |
| # Com VERTEX COLORS: topo levemente mais claro; ponta do dente mais escura | |
| # ----- Parâmetros do bloco ----- | |
| @export var height_y: float = 2.5 | |
| @export var base_size: Vector2 = Vector2(4.5, 4.5) | |
| @export var top_size: Vector2 = Vector2(6.0, 6.0) | |
| # ----- Cores ----- | |
| @export var dirt_color: Color = Color(0.45, 0.25, 0.12) | |
| @export var grass_color: Color = Color(0.25, 0.70, 0.55, 1.0) | |
| # ----- Tampa / serrilha ----- | |
| @export var cap_thickness: float = 0.14 # altura do tampo acima do topo | |
| @export var skirt_height: float = 0.55 # quanto descem os “dentes” | |
| @export var teeth_per_side: int = 10 # >= 2 (usa 2*teeth segmentos por lado) | |
| @export var overhang_xy: float = 0.0 # 0 = alinhado; >0 avança um pouco | |
| @export var debug_disable_cull := false # útil pra depurar faces | |
| # ----- Câmera orbital ----- | |
| var _cam: Camera3D | |
| var _target := Vector3.ZERO | |
| var _pan_vector := Vector3.ZERO | |
| var _orbit_distance := 10.0 | |
| var _orbit_angle_h := 0.0 | |
| var _orbit_angle_v := 0.0 | |
| var _dragging_orbit := false | |
| var _dragging_pan := false | |
| var _zoom_speed := 0.5 | |
| func _ready() -> void: | |
| _add_environment() | |
| _add_light() | |
| _cam = _add_camera() | |
| # ---- materiais ---- | |
| var mat_dirt := StandardMaterial3D.new() | |
| mat_dirt.albedo_color = dirt_color | |
| mat_dirt.roughness = 0.8 | |
| mat_dirt.metallic = 0.0 | |
| var mat_grass := StandardMaterial3D.new() | |
| # vamos pintar via cor de vértice, então o albedo base é multiplicador neutro | |
| mat_grass.albedo_color = Color(1, 1, 1) | |
| mat_grass.roughness = 0.45 | |
| mat_grass.metallic = 0.0 | |
| mat_grass.vertex_color_use_as_albedo = true | |
| mat_grass.vertex_color_is_srgb = true | |
| mat_grass.cull_mode = BaseMaterial3D.CULL_DISABLED if debug_disable_cull else BaseMaterial3D.CULL_BACK | |
| # ---- corpo (terra) ---- | |
| var dirt := MeshInstance3D.new() | |
| dirt.mesh = _create_flared_box_mesh(height_y, base_size, top_size) | |
| dirt.material_override = mat_dirt | |
| add_child(dirt) | |
| # ---- tampa + saia (grama em 2 superfícies) ---- | |
| var cap := MeshInstance3D.new() | |
| cap.mesh = _create_cap_with_welded_skirt(height_y, top_size, cap_thickness, skirt_height, teeth_per_side, overhang_xy) | |
| # surface 0 = topo / surface 1 = saia | |
| cap.set_surface_override_material(0, mat_grass) | |
| cap.set_surface_override_material(1, mat_grass) | |
| add_child(cap) | |
| func _process(_dt: float) -> void: | |
| _update_camera() | |
| # ========================= MALHA DO BLOCO ========================= | |
| func _create_flared_box_mesh(h: float, base_xy: Vector2, top_xy: Vector2) -> ArrayMesh: | |
| var bx := base_xy.x * 0.5 | |
| var bz := base_xy.y * 0.5 | |
| var tx := top_xy.x * 0.5 | |
| var tz := top_xy.y * 0.5 | |
| var hy := h * 0.5 | |
| var b0 := Vector3(-bx, -hy, -bz) | |
| var b1 := Vector3( bx, -hy, -bz) | |
| var b2 := Vector3( bx, -hy, bz) | |
| var b3 := Vector3(-bx, -hy, bz) | |
| var t0 := Vector3(-tx, hy, -tz) | |
| var t1 := Vector3( tx, hy, -tz) | |
| var t2 := Vector3( tx, hy, tz) | |
| var t3 := Vector3(-tx, hy, tz) | |
| var st := SurfaceTool.new() | |
| st.begin(Mesh.PRIMITIVE_TRIANGLES) | |
| # base | |
| _add_tri(st, b0, b2, b1) | |
| _add_tri(st, b0, b3, b2) | |
| # topo do corpo (coberto pela tampa) | |
| _add_tri(st, t0, t1, t2) | |
| _add_tri(st, t0, t2, t3) | |
| # lados | |
| _add_tri(st, b0, b1, t1); _add_tri(st, b0, t1, t0) # frente | |
| _add_tri(st, b1, b2, t2); _add_tri(st, b1, t2, t1) # direita | |
| _add_tri(st, b2, b3, t3); _add_tri(st, b2, t3, t2) # fundo | |
| _add_tri(st, b3, b0, t0); _add_tri(st, b3, t0, t3) # esquerda | |
| st.generate_normals() | |
| st.index() | |
| return st.commit() | |
| # ============== TAMPA SOLDADA (topo + saia com anel compartilhado) ============== | |
| # 2 superfícies: 0 = topo (leque), 1 = saia (quads) | |
| # Com vertex colors: topo mais claro; ponta do dente mais escura | |
| func _create_cap_with_welded_skirt(h: float, top_xy: Vector2, t: float, skirt: float, teeth: int, over: float) -> ArrayMesh: | |
| teeth = max(2, teeth) | |
| var tx := (top_xy.x * 0.5) + over | |
| var tz := (top_xy.y * 0.5) + over | |
| var y_top := h * 0.5 + t | |
| var y_edge := h * 0.5 | |
| var segs_side := teeth * 2 | |
| var dx := (tx * 2.0) / float(segs_side) | |
| var dz := (tz * 2.0) / float(segs_side) | |
| # ---------- anel CCW (visto de cima) ---------- | |
| var ring: Array[Vector3] = [] | |
| # esquerda: x=-tx, z:-tz..+tz | |
| for i in range(segs_side): | |
| ring.append(Vector3(-tx, y_top, -tz + dz * float(i))) | |
| ring.append(Vector3(-tx, y_top, +tz)) | |
| # fundo: z=+tz, x:-tx..+tx | |
| for i in range(1, segs_side + 1): | |
| ring.append(Vector3(-tx + dx * float(i), y_top, +tz)) | |
| # direita: x=+tx, z:+tz..-tz | |
| for i in range(1, segs_side + 1): | |
| ring.append(Vector3(+tx, y_top, +tz - dz * float(i))) | |
| # frente: z=-tz, x:+tx..-tx | |
| for i in range(1, segs_side + 1): | |
| ring.append(Vector3(+tx - dx * float(i), y_top, -tz)) | |
| # o último coincide com o primeiro (-tx,-tz) | |
| var mesh := ArrayMesh.new() | |
| # ---------- paleta da grama ---------- | |
| var grass_base := grass_color | |
| var grass_top := grass_base.lightened(0.08) # topo levemente mais claro | |
| var grass_mid := grass_base # borda | |
| var grass_tip := grass_base.darkened(0.20) # ponta do dente mais escura | |
| # ---------- Superfície 0: TOPO (leque) ---------- | |
| var st_top := SurfaceTool.new() | |
| st_top.begin(Mesh.PRIMITIVE_TRIANGLES) | |
| var center := Vector3(0, y_top, 0) | |
| for i in range(ring.size()): | |
| var a := ring[i] | |
| var b := ring[(i + 1) % ring.size()] | |
| _add_tri_colored(st_top, center, b, a, grass_top, grass_mid, grass_mid) | |
| st_top.generate_normals() | |
| st_top.index() | |
| st_top.commit(mesh) # surface 0 | |
| # ---------- Superfície 1: SAIA (quads CCW) ---------- | |
| var st_skirt := SurfaceTool.new() | |
| st_skirt.begin(Mesh.PRIMITIVE_TRIANGLES) | |
| for i in range(ring.size()): | |
| var top0 := ring[i] | |
| var top1 := ring[(i + 1) % ring.size()] | |
| var yb0 := (y_edge - skirt) if ((i % 2) == 0) else y_edge | |
| var yb1 := (y_edge - skirt) if (((i + 1) % 2) == 0) else y_edge | |
| var bot0 := Vector3(top0.x, yb0, top0.z) | |
| var bot1 := Vector3(top1.x, yb1, top1.z) | |
| # cores por vértice: clareia na borda, escurece se for ponta (quando desce) | |
| var c_top0 := grass_mid | |
| var c_top1 := grass_mid | |
| var c_bot0 := (grass_tip if (yb0 < y_edge) else grass_mid) | |
| var c_bot1 := (grass_tip if (yb1 < y_edge) else grass_mid) | |
| _add_rect_ccw_colored(st_skirt, top0, top1, bot1, bot0, c_top0, c_top1, c_bot1, c_bot0) | |
| st_skirt.generate_normals() | |
| st_skirt.index() | |
| st_skirt.commit(mesh) # surface 1 | |
| return mesh | |
| # ========================= HELPERS (geom) ========================= | |
| func _yb_alternate(idx: int, y_edge: float, skirt: float) -> float: | |
| return y_edge - (skirt if (idx % 2) == 0 else 0.0) | |
| func _add_tri(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3) -> void: | |
| st.add_vertex(a); st.add_vertex(b); st.add_vertex(c) | |
| func _add_rect_ccw(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> void: | |
| # a,b,c,d em sentido anti-horário visto de fora | |
| st.add_vertex(a); st.add_vertex(b); st.add_vertex(c) | |
| st.add_vertex(a); st.add_vertex(c); st.add_vertex(d) | |
| # ===== HELPERS com COLOR (usar set_color antes de add_vertex) ===== | |
| func _add_tri_colored(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, ca: Color, cb: Color, cc: Color) -> void: | |
| st.set_color(ca); st.add_vertex(a) | |
| st.set_color(cb); st.add_vertex(b) | |
| st.set_color(cc); st.add_vertex(c) | |
| func _add_rect_ccw_colored(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, d: Vector3, | |
| ca: Color, cb: Color, cc: Color, cd: Color) -> void: | |
| # a,b,c,d em CCW | |
| st.set_color(ca); st.add_vertex(a) | |
| st.set_color(cb); st.add_vertex(b) | |
| st.set_color(cc); st.add_vertex(c) | |
| st.set_color(ca); st.add_vertex(a) | |
| st.set_color(cc); st.add_vertex(c) | |
| st.set_color(cd); st.add_vertex(d) | |
| # ==================== AMBIENTE / LUZ / CÂMERA ==================== | |
| func _add_environment() -> void: | |
| var we := WorldEnvironment.new() | |
| var env := Environment.new() | |
| var psky := ProceduralSkyMaterial.new() | |
| psky.sky_top_color = Color(0.62, 0.78, 1.0) | |
| psky.sky_horizon_color = Color(0.9, 0.95, 1.0) | |
| psky.ground_bottom_color = Color(0.95, 0.88, 0.78) | |
| var sky := Sky.new() | |
| sky.sky_material = psky | |
| env.background_mode = Environment.BG_SKY | |
| env.sky = sky | |
| env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY | |
| env.ssao_enabled = true | |
| env.ssao_intensity = 0.9 | |
| we.environment = env | |
| add_child(we) | |
| func _add_light() -> void: | |
| var sun := DirectionalLight3D.new() | |
| sun.light_energy = 2.0 | |
| sun.light_indirect_energy = 0.4 | |
| sun.shadow_enabled = true | |
| sun.directional_shadow_max_distance = 100.0 | |
| sun.rotation_degrees = Vector3(45, -30, 0) | |
| add_child(sun) | |
| func _add_camera() -> Camera3D: | |
| var cam := Camera3D.new() | |
| var initial_pos := Vector3(8, 6, 8) | |
| _orbit_distance = initial_pos.length() | |
| var horiz := Vector2(initial_pos.x, initial_pos.z) | |
| _orbit_angle_h = atan2(horiz.y, horiz.x) | |
| _orbit_angle_v = asin(clampf(initial_pos.y / _orbit_distance, -1.0, 1.0)) | |
| add_child(cam) | |
| return cam | |
| func _update_camera() -> void: | |
| if _cam == null: | |
| return | |
| var look_center := _target + _pan_vector | |
| var dir := Vector3( | |
| cos(_orbit_angle_h) * cos(_orbit_angle_v), | |
| sin(_orbit_angle_v), | |
| sin(_orbit_angle_h) * cos(_orbit_angle_v) | |
| ).normalized() | |
| _cam.position = look_center + dir * _orbit_distance | |
| _cam.look_at(look_center, Vector3.UP) | |
| func _input(event: InputEvent) -> void: | |
| if event is InputEventMouseMotion: | |
| var delta := (event as InputEventMouseMotion).relative | |
| if _dragging_orbit: | |
| _orbit_angle_h -= delta.x * 0.005 | |
| _orbit_angle_v -= delta.y * 0.005 | |
| _orbit_angle_v = clampf(_orbit_angle_v, -PI/2 + 0.1, PI/2 - 0.1) | |
| elif _dragging_pan: | |
| var right := Vector3(cos(_orbit_angle_h), 0, sin(_orbit_angle_h)) | |
| _pan_vector -= right * delta.x * 0.01 | |
| _pan_vector -= Vector3.UP * delta.y * 0.01 * (_orbit_distance / 10.0) | |
| elif event is InputEventMouseButton: | |
| var mb := event as InputEventMouseButton | |
| match mb.button_index: | |
| MOUSE_BUTTON_MIDDLE: | |
| if mb.pressed: | |
| _dragging_orbit = not Input.is_key_pressed(KEY_SHIFT) | |
| _dragging_pan = Input.is_key_pressed(KEY_SHIFT) | |
| Input.mouse_mode = Input.MOUSE_MODE_CAPTURED | |
| else: | |
| _dragging_orbit = false | |
| _dragging_pan = false | |
| Input.mouse_mode = Input.MOUSE_MODE_VISIBLE | |
| MOUSE_BUTTON_WHEEL_UP: | |
| _orbit_distance = maxf(_orbit_distance * (1.0 - _zoom_speed * 0.1), 1.0) | |
| MOUSE_BUTTON_WHEEL_DOWN: | |
| _orbit_distance *= (1.0 + _zoom_speed * 0.1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment