This commit is contained in:
team3
2026-09-16 17:00:41 +02:00
parent f1c78f66f4
commit 9800c614bb
21 changed files with 497 additions and 191 deletions

View File

@@ -1,129 +1,145 @@
extends RefCounted
## Materialien ohne Texturdateien: Holz, Filz und Messing werden im Shader
## aus Rauschen gerechnet. Alles, was Maserung, Flecken oder Anlauf ist, sind
## Zahlen - keine Bilder. Siehe assets.md. Kein class_name (headless kein Cache).
## Materialien aus Regeln, ohne eine einzige Datei. Je Stoff ein Eintrag:
## Farbverlauf, Rauschart, Reliefstärke, Rauheit, Maßstab. Daraus baut
## `mach()` ein StandardMaterial3D mit drei zur Laufzeit erzeugten Texturen:
## Farbe (Rauschen durch einen Farbverlauf), Relief (dasselbe Rauschen als
## Normalenkarte) und Rauheit. Das Relief ist der Unterschied zwischen
## "gemustertes Plastik" und Filz, Holz, Messing.
##
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
const RAUSCHEN := """
float hash2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise2(vec2 p) {
vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash2(i), hash2(i + vec2(1, 0)), f.x),
mix(hash2(i + vec2(0, 1)), hash2(i + vec2(1, 1)), f.x), f.y);
const STOFFE := {
"filz": {
# Feiner Flaum, kaum Relief, ganz matt. Grobes, glänzendes Relief sah
# nach nassem Moos aus.
"farben": [Color(0.06, 0.18, 0.11), Color(0.08, 0.23, 0.14), Color(0.10, 0.26, 0.16)],
"art": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "freq": 0.35, "oktaven": 3,
"relief": 0.12, "rauheit": [0.96, 1.0], "metall": 0.0, "uv": Vector3(14, 14, 1),
"glanz": 0.05,
},
"holz": {
"farben": [Color(0.20, 0.12, 0.06), Color(0.36, 0.23, 0.11), Color(0.46, 0.31, 0.16)],
"art": FastNoiseLite.TYPE_SIMPLEX, "freq": 0.03, "oktaven": 5, "warp": 25.0,
"relief": 0.6, "rauheit": [0.45, 0.75], "metall": 0.0,
# stark gestreckt: aus dem Rauschen wird Maserung
"uv": Vector3(1.0, 9.0, 1.0),
},
"holz_dunkel": {
"farben": [Color(0.14, 0.06, 0.04), Color(0.30, 0.12, 0.07), Color(0.40, 0.18, 0.10)],
"art": FastNoiseLite.TYPE_SIMPLEX, "freq": 0.03, "oktaven": 5, "warp": 25.0,
"relief": 0.5, "rauheit": [0.4, 0.7], "metall": 0.0, "uv": Vector3(1.0, 7.0, 1.0),
},
"messing": {
"farben": [Color(0.55, 0.40, 0.14), Color(0.80, 0.62, 0.26), Color(0.95, 0.80, 0.42)],
"art": FastNoiseLite.TYPE_CELLULAR, "freq": 0.12, "oktaven": 2,
"relief": 0.25, "rauheit": [0.2, 0.5], "metall": 0.9, "uv": Vector3(3, 3, 1),
},
"knochen": {
# Farbe kommt von den Vertexfarben (Körper hell, Augen dunkel); hier
# nur feine Dellen und eine warme, leicht fettige Oberfläche.
"farben": [], "art": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "freq": 0.25, "oktaven": 3,
"relief": 0.2, "rauheit": [0.45, 0.65], "metall": 0.0, "uv": Vector3(4, 4, 1),
},
}
float fbm2(vec2 p) {
float v = 0.0; float a = 0.5;
for (int k = 0; k < 4; k++) { v += a * noise2(p); p = p * 2.03 + 17.0; a *= 0.5; }
return v;
}
"""
## Holz: Jahresringe entlang einer Achse, leicht verzerrt, plus feine
## Faser. Weltkoordinaten, damit die Maserung über Teile hinweg durchläuft.
const HOLZ := """
shader_type spatial;
uniform vec4 base_color : source_color = vec4(0.42, 0.27, 0.13, 1.0);
uniform vec4 dark_color : source_color = vec4(0.22, 0.13, 0.06, 1.0);
uniform float wear : hint_range(0.0, 1.0) = 0.4;
uniform float ring_scale = 9.0;
varying vec3 wp;
""" + RAUSCHEN + """
void vertex() { wp = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
void fragment() {
vec2 p = wp.xz;
float verzerrt = p.x * ring_scale + fbm2(p * 2.5) * 3.0;
float ring = smoothstep(0.2, 0.8, abs(fract(verzerrt) - 0.5) * 2.0);
float faser = noise2(vec2(p.x * 60.0, p.y * 4.0)) * 0.12;
vec3 col = mix(dark_color.rgb, base_color.rgb, ring) + faser - 0.06;
float flecken = fbm2(p * 6.0 + 40.0);
col = mix(col, col * 0.75, smoothstep(0.55, 0.8, flecken) * wear);
ALBEDO = col;
ROUGHNESS = 0.55 + ring * 0.25 + wear * 0.15;
SPECULAR = 0.3;
}
"""
## Filz: gleichmässige Fläche mit feinem Flaum, leicht fleckig, an den
## Rändern dunkler. Ohne das sähe die Zone aus wie Plastik.
const FILZ := """
shader_type spatial;
uniform vec4 base_color : source_color = vec4(0.07, 0.20, 0.13, 1.0);
uniform float wear : hint_range(0.0, 1.0) = 0.5;
uniform float radius = 0.68;
varying vec3 lp;
varying vec3 wp;
""" + RAUSCHEN + """
void vertex() { lp = VERTEX; wp = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
void fragment() {
float flaum = noise2(wp.xz * 90.0) * 0.10 + noise2(wp.xz * 300.0) * 0.05;
float flecken = fbm2(wp.xz * 3.0) ;
float rand = smoothstep(radius * 0.7, radius, length(lp.xz));
vec3 col = base_color.rgb * (0.9 + flaum) * (1.0 - flecken * 0.25 * wear) * (1.0 - rand * 0.25);
ALBEDO = col;
ROUGHNESS = 0.97;
SPECULAR = 0.1;
}
"""
## Messing: metallisch, mit Anlauf in den Vertiefungen und einem Hauch
## Farbschwankung. Die Prägung bleibt ein eigenes Mesh.
const MESSING := """
shader_type spatial;
uniform vec4 base_color : source_color = vec4(0.80, 0.62, 0.26, 1.0);
uniform float wear : hint_range(0.0, 1.0) = 0.4;
uniform float emission : hint_range(0.0, 2.0) = 0.35;
varying vec3 wp;
""" + RAUSCHEN + """
void vertex() { wp = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
void fragment() {
float anlauf = fbm2(wp.xz * 40.0);
vec3 col = base_color.rgb * (0.85 + anlauf * 0.3);
col = mix(col, col * vec3(0.7, 0.6, 0.5), smoothstep(0.6, 0.85, anlauf) * wear);
ALBEDO = col;
METALLIC = 0.9;
ROUGHNESS = 0.25 + anlauf * 0.35 * wear;
EMISSION = base_color.rgb * 0.6 * emission;
}
"""
static var _holz: Shader
static var _filz: Shader
static var _messing: Shader
static var _fertig: Dictionary = {} ## name -> StandardMaterial3D, einmal gebaut
static func _shader(code: String) -> Shader:
var s := Shader.new()
s.code = code
return s
static func _rauschen(r: Dictionary, seed: int) -> FastNoiseLite:
var n := FastNoiseLite.new()
n.seed = seed
n.noise_type = r.art
n.frequency = r.freq
n.fractal_octaves = r.oktaven
if r.has("warp"):
n.domain_warp_enabled = true
n.domain_warp_amplitude = r.warp
return n
static func holz(farbe: Color, dunkel: Color, wear := 0.4, ring_scale := 9.0) -> ShaderMaterial:
if _holz == null:
_holz = _shader(HOLZ)
var m := ShaderMaterial.new()
m.shader = _holz
m.set_shader_parameter("base_color", farbe)
m.set_shader_parameter("dark_color", dunkel)
m.set_shader_parameter("wear", wear)
m.set_shader_parameter("ring_scale", ring_scale)
static func _textur(n: FastNoiseLite, groesse: int) -> NoiseTexture2D:
var t := NoiseTexture2D.new()
t.noise = n
t.width = groesse
t.height = groesse
t.seamless = true
t.generate_mipmaps = true
return t
## Baut das Material nach Regel `name`. Gleiche Regel, gleiches Material:
## alle Becher teilen eine Instanz, so bleibt es ein Renderobjekt je Stoff.
static func mach(name: String) -> StandardMaterial3D:
if _fertig.has(name):
return _fertig[name]
var r: Dictionary = STOFFE[name]
var m := StandardMaterial3D.new()
var basis := _rauschen(r, name.hash() % 1000)
if not r.farben.is_empty():
var verlauf := Gradient.new()
var farben: Array = r.farben
verlauf.offsets = PackedFloat32Array([0.0, 0.5, 1.0])
verlauf.colors = PackedColorArray([farben[0], farben[1], farben[2]])
var farbe := _textur(basis, 256)
farbe.color_ramp = verlauf
m.albedo_texture = farbe
else:
m.vertex_color_use_as_albedo = true
var relief := _textur(basis, 256)
relief.as_normal_map = true
relief.bump_strength = float(r.relief) * 8.0
m.normal_enabled = true
m.normal_texture = relief
m.normal_scale = 1.0
# Rauheit schwankt mit dem Rauschen zwischen den zwei Werten.
var rau := _textur(_rauschen(r, name.hash() % 1000 + 7), 128)
m.roughness_texture = rau
m.roughness_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
m.roughness = float(r.rauheit[1])
m.metallic = float(r.metall)
m.metallic_specular = float(r.get("glanz", 0.5 if r.metall > 0.5 else 0.3))
m.uv1_scale = r.uv
m.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
_fertig[name] = m
return m
static func filz(farbe: Color, radius: float, wear := 0.5) -> ShaderMaterial:
if _filz == null:
_filz = _shader(FILZ)
var m := ShaderMaterial.new()
m.shader = _filz
m.set_shader_parameter("base_color", farbe)
m.set_shader_parameter("radius", radius)
m.set_shader_parameter("wear", wear)
## Eigene Instanz, wenn ein Objekt Farbe oder Leuchten ändern muss.
static func eigene(name: String) -> StandardMaterial3D:
return mach(name).duplicate()
static func filz() -> StandardMaterial3D:
return mach("filz")
static func holz() -> StandardMaterial3D:
return mach("holz")
static func holz_dunkel() -> StandardMaterial3D:
return mach("holz_dunkel")
static func messing(emission := 0.0) -> StandardMaterial3D:
var m := eigene("messing")
if emission > 0.0:
m.emission_enabled = true
m.emission = Color(1.0, 0.75, 0.30)
m.emission_energy_multiplier = emission
return m
static func messing(emission := 0.35, wear := 0.4) -> ShaderMaterial:
if _messing == null:
_messing = _shader(MESSING)
var m := ShaderMaterial.new()
m.shader = _messing
m.set_shader_parameter("emission", emission)
m.set_shader_parameter("wear", wear)
static func knochen() -> StandardMaterial3D:
return mach("knochen")
## Zonenrand: Holz, das leuchten kann, wenn der Platz dran ist.
static func rand() -> StandardMaterial3D:
var m := eigene("holz")
m.emission = Color(1.0, 0.78, 0.30)
m.emission_enabled = false
return m