# -*- coding: utf-8 -*-
# RUST-style IRON ORE node - same boulder породы as stone_node, bronze/brown + metallic veins
import bpy, bmesh, math, os, random
from mathutils import Vector, noise

SEED        = 12                       # same seed => same silhouette as stone_node
OUT         = "/opt/models/iron_node"
NAME        = "iron_node"
TEX_SIZE    = 2048
TARGET_TRIS = 4800

random.seed(SEED)
os.makedirs(OUT, exist_ok=True)

bpy.ops.wm.read_factory_settings(use_empty=True)
sc = bpy.context.scene
sc.render.engine = 'CYCLES'
sc.cycles.device = 'CPU'
sc.unit_settings.system = 'METRIC'

# ---------------------------------------------------------------- shape ----
L = Vector((0.36, 0.30, 0.88)).normalized()   # tilted bedding planes (strata)

CUTS = [
    (Vector(( 0.62,  0.55,  0.56)).normalized(), 0.86),
    (Vector((-0.72,  0.30,  0.42)).normalized(), 0.82),
    (Vector(( 0.10, -0.86,  0.50)).normalized(), 0.84),
    (Vector((-0.30, -0.45, -0.84)).normalized(), 0.66),
]

def n01(v, scale, off, oct=3):
    return min(1.0, max(0.0, 0.5 + 0.5 * noise.fractal(v * scale + off, 1.0, 2.0, oct)))

def surface_offset(p):
    lump = noise.fractal(p * 0.80 + Vector(( 13.0,  2.0,  7.0)), 1.0, 2.0, 4) * 0.265
    lump += noise.fractal(p * 0.42 + Vector((41.0, 11.0, 23.0)), 1.0, 2.0, 2) * 0.150
    med  = noise.fractal(p * 2.10 + Vector((  0.0, 21.0,  3.0)), 0.75, 2.0, 4) * 0.070
    across  = 0.32 + 0.68 * (1.0 - abs(p.dot(L))) ** 0.8
    expose  = 0.30 + 0.85 * n01(p, 0.75, Vector((31.0, 4.0, 9.0)))
    erosion = 0.30 + 0.70 * min(1.0, max(0.0, (p.z + 0.80) / 0.95))
    amp     = min(1.0, expose) * erosion * across
    period  = 1.55 + 1.15 * n01(p, 0.55, Vector((7.0, 17.0, 2.0)))
    warp    = noise.fractal(p * 1.05 + Vector((5.0, 5.0, 5.0)), 1.0, 2.0, 4) * 1.10
    t       = (p.dot(L)) * period + warp
    f       = t - math.floor(t)
    plate   = ((f ** 0.32) * 0.150 - 0.070) * amp
    f2      = (t * 3.2) - math.floor(t * 3.2)
    plate  += ((f2 ** 0.55) * 0.026 - 0.013) * amp
    dists, _ = noise.voronoi(p * 2.60, distance_metric='DISTANCE')
    chip = -dists[0] * 0.060
    dists2, _ = noise.voronoi(p * 5.80 + Vector((9.0, 1.0, 4.0)), distance_metric='DISTANCE')
    chip += -dists2[0] * 0.020 * (0.4 + 0.6 * erosion)
    micro = noise.fractal(p * 10.0, 0.9, 2.0, 3) * 0.012
    return lump + med + plate + chip + micro

def build_rock(subdiv, name):
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdiv, radius=1.0)
    ob = bpy.context.object
    ob.name = name
    me = ob.data
    bm = bmesh.new(); bm.from_mesh(me)
    for v in bm.verts:
        n = v.co.normalized()
        p = Vector((n.x * 1.00, n.y * 0.88, n.z * 0.64))
        p = p + n * surface_offset(n)
        p.x += p.z * 0.12
        for cn, cd in CUTS:
            s = p.dot(cn) - cd
            if s > 0.0:
                p = p - cn * s
                p += cn * (noise.fractal(p * 6.0, 0.8, 2.0, 3) * 0.010)
        zmin = -0.44
        if p.z < zmin:
            p.z = zmin + (p.z - zmin) * 0.14
        v.co = p
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bm.to_mesh(me); bm.free()
    me.update()
    return ob

high = build_rock(6, NAME + "_high")
low = high.copy(); low.data = high.data.copy(); low.name = NAME
low.data.name = NAME
sc.collection.objects.link(low)
bpy.context.view_layer.objects.active = low
dec = low.modifiers.new("dec", 'DECIMATE')
dec.ratio = TARGET_TRIS / len(low.data.polygons)
bpy.ops.object.modifier_apply(modifier="dec")

def smooth_by_angle(ob, deg=32.0):
    bpy.ops.object.select_all(action='DESELECT')
    ob.select_set(True)
    bpy.context.view_layer.objects.active = ob
    bpy.ops.object.shade_smooth()
    try:
        bpy.ops.object.shade_smooth_by_angle(angle=math.radians(deg))
    except Exception as e:
        print("shade_smooth_by_angle:", e)

for ob in (high, low):
    smooth_by_angle(ob)

zmin = min((low.matrix_world @ Vector(c)).z for c in low.bound_box)
sc.cursor.location = (0.0, 0.0, zmin)
bpy.ops.object.select_all(action='DESELECT')
high.select_set(True); low.select_set(True)
bpy.context.view_layer.objects.active = low
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
sc.cursor.location = (0, 0, 0)
for ob in (high, low):
    ob.location = (0, 0, 0)

# ------------------------------------------------------------------ UVs ---
bpy.ops.object.select_all(action='DESELECT')
low.select_set(True)
bpy.context.view_layer.objects.active = low
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.smart_project(angle_limit=math.radians(89), island_margin=0.004,
                         area_weight=0.0, correct_aspect=True, scale_to_bounds=False)
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.average_islands_scale()
bpy.ops.uv.pack_islands(margin=0.004, rotate=True)
bpy.ops.object.mode_set(mode='OBJECT')

# ------------------------------------------------------ iron ore material --
def iron_material():
    mat = bpy.data.materials.new(NAME + "_proc")
    mat.use_nodes = True
    nt = mat.node_tree
    nt.nodes.clear()
    n = nt.nodes.new
    out  = n('ShaderNodeOutputMaterial'); out.location = (1700, 0)
    bsdf = n('ShaderNodeBsdfPrincipled'); bsdf.location = (1400, 0)
    nt.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])

    tc = n('ShaderNodeTexCoord'); tc.location = (-1800, 0)

    def banding(freq, warp_scale, warp_amt, loc_y):
        dot = n('ShaderNodeVectorMath'); dot.operation = 'DOT_PRODUCT'
        dot.location = (-1600, loc_y)
        dot.inputs[1].default_value = (L.x, L.y, L.z)
        nt.links.new(tc.outputs['Object'], dot.inputs[0])
        wn = n('ShaderNodeTexNoise'); wn.location = (-1600, loc_y - 180)
        wn.inputs['Scale'].default_value = warp_scale
        wn.inputs['Detail'].default_value = 7.0
        wn.inputs['Roughness'].default_value = 0.62
        nt.links.new(tc.outputs['Object'], wn.inputs['Vector'])
        m1 = n('ShaderNodeMath'); m1.operation = 'MULTIPLY'; m1.location = (-1400, loc_y)
        m1.inputs[1].default_value = freq
        nt.links.new(dot.outputs['Value'], m1.inputs[0])
        m2 = n('ShaderNodeMath'); m2.operation = 'MULTIPLY'; m2.location = (-1400, loc_y - 180)
        m2.inputs[1].default_value = warp_amt
        nt.links.new(wn.outputs['Fac'], m2.inputs[0])
        ad = n('ShaderNodeMath'); ad.operation = 'ADD'; ad.location = (-1220, loc_y - 90)
        nt.links.new(m1.outputs['Value'], ad.inputs[0])
        nt.links.new(m2.outputs['Value'], ad.inputs[1])
        fr = n('ShaderNodeMath'); fr.operation = 'FRACT'; fr.location = (-1060, loc_y - 90)
        nt.links.new(ad.outputs['Value'], fr.inputs[0])
        return fr

    # ---- host rock: iron-stained brown schist -------------------------------
    fr = banding(4.2, 0.85, 0.80, 300)
    ramp = n('ShaderNodeValToRGB'); ramp.location = (-880, 210)
    cr = ramp.color_ramp
    cr.interpolation = 'LINEAR'
    stops = [
        (0.00, (0.045, 0.026, 0.013)),
        (0.16, (0.084, 0.050, 0.021)),
        (0.25, (0.010, 0.006, 0.003)),
        (0.34, (0.017, 0.010, 0.005)),
        (0.42, (0.056, 0.033, 0.015)),
        (0.58, (0.101, 0.061, 0.025)),
        (0.67, (0.017, 0.010, 0.005)),
        (0.76, (0.035, 0.021, 0.009)),
        (0.88, (0.073, 0.044, 0.019)),
    ]
    cr.elements[0].position = stops[0][0]; cr.elements[0].color = (*stops[0][1], 1)
    cr.elements[1].position = stops[1][0]; cr.elements[1].color = (*stops[1][1], 1)
    for pos, col in stops[2:]:
        e = cr.elements.new(pos); e.color = (*col, 1)
    nt.links.new(fr.outputs['Value'], ramp.inputs['Fac'])

    fr2 = banding(19.0, 2.4, 0.5, -160)
    ramp2 = n('ShaderNodeValToRGB'); ramp2.location = (-880, -250)
    cr2 = ramp2.color_ramp
    cr2.elements[0].position = 0.30; cr2.elements[0].color = (0.70, 0.70, 0.70, 1)
    cr2.elements[1].position = 0.62; cr2.elements[1].color = (1.18, 1.18, 1.18, 1)
    nt.links.new(fr2.outputs['Value'], ramp2.inputs['Fac'])
    mixL = n('ShaderNodeMixRGB'); mixL.blend_type = 'MULTIPLY'; mixL.location = (-640, 100)
    mixL.inputs['Fac'].default_value = 0.30
    nt.links.new(ramp.outputs['Color'],  mixL.inputs['Color1'])
    nt.links.new(ramp2.outputs['Color'], mixL.inputs['Color2'])

    mkn = n('ShaderNodeTexNoise'); mkn.location = (-640, -380)
    mkn.inputs['Scale'].default_value = 1.9
    mkn.inputs['Detail'].default_value = 5.0
    nt.links.new(tc.outputs['Object'], mkn.inputs['Vector'])
    mkr = n('ShaderNodeValToRGB'); mkr.location = (-470, -380)
    mkr.color_ramp.elements[0].position = 0.38
    mkr.color_ramp.elements[1].position = 0.62
    nt.links.new(mkn.outputs['Fac'], mkr.inputs['Fac'])
    mixB = n('ShaderNodeMixRGB'); mixB.blend_type = 'MIX'; mixB.location = (-540, 60)
    mixB.inputs['Color1'].default_value = (0.050, 0.030, 0.015, 1)   # plain worn brown rock
    nt.links.new(mkr.outputs['Color'], mixB.inputs['Fac'])
    nt.links.new(mixL.outputs['Color'], mixB.inputs['Color2'])

    # rusty / ochre patches
    big = n('ShaderNodeTexNoise'); big.location = (-880, -620)
    big.inputs['Scale'].default_value = 1.0
    big.inputs['Detail'].default_value = 4.0
    nt.links.new(tc.outputs['Object'], big.inputs['Vector'])
    bigr = n('ShaderNodeValToRGB'); bigr.location = (-660, -620)
    bigr.color_ramp.elements[0].position = 0.34
    bigr.color_ramp.elements[0].color = (0.205, 0.085, 0.028, 1)   # rust
    bigr.color_ramp.elements[1].position = 0.66
    bigr.color_ramp.elements[1].color = (0.118, 0.090, 0.064, 1)   # dusty grey-brown
    nt.links.new(big.outputs['Fac'], bigr.inputs['Fac'])
    mix1 = n('ShaderNodeMixRGB'); mix1.blend_type = 'OVERLAY'; mix1.location = (-380, 0)
    mix1.inputs['Fac'].default_value = 0.38
    nt.links.new(mixB.outputs['Color'], mix1.inputs['Color1'])
    nt.links.new(bigr.outputs['Color'], mix1.inputs['Color2'])

    # crevice dirt / worn bright edges
    geo = n('ShaderNodeNewGeometry'); geo.location = (-660, -950)
    pr  = n('ShaderNodeValToRGB'); pr.location = (-460, -950)
    pr.color_ramp.elements[0].position = 0.40
    pr.color_ramp.elements[0].color = (0.30, 0.22, 0.16, 1)
    pr.color_ramp.elements[1].position = 0.58
    pr.color_ramp.elements[1].color = (1.05, 1.01, 0.95, 1)
    nt.links.new(geo.outputs['Pointiness'], pr.inputs['Fac'])
    mix2 = n('ShaderNodeMixRGB'); mix2.blend_type = 'MULTIPLY'; mix2.location = (-160, 0)
    mix2.inputs['Fac'].default_value = 0.45
    nt.links.new(mix1.outputs['Color'], mix2.inputs['Color1'])
    nt.links.new(pr.outputs['Color'], mix2.inputs['Color2'])

    # mineral speckle in the host rock
    spk = n('ShaderNodeTexVoronoi'); spk.location = (-160, -560)
    spk.inputs['Scale'].default_value = 120.0
    nt.links.new(tc.outputs['Object'], spk.inputs['Vector'])
    spkr = n('ShaderNodeValToRGB'); spkr.location = (40, -560)
    spkr.color_ramp.elements[0].position = 0.0
    spkr.color_ramp.elements[0].color = (0.84, 0.82, 0.80, 1)
    spkr.color_ramp.elements[1].position = 0.55
    spkr.color_ramp.elements[1].color = (1.10, 1.08, 1.05, 1)
    nt.links.new(spk.outputs['Distance'], spkr.inputs['Fac'])
    host = n('ShaderNodeMixRGB'); host.blend_type = 'MULTIPLY'; host.location = (240, 0)
    host.inputs['Fac'].default_value = 0.30
    nt.links.new(mix2.outputs['Color'], host.inputs['Color1'])
    nt.links.new(spkr.outputs['Color'], host.inputs['Color2'])

    # ---- ORE MASK: metallic veins следуют тем же пластам --------------------
    ovr = n('ShaderNodeValToRGB'); ovr.location = (-880, 640)      # veins in some layers only
    ovr.color_ramp.interpolation = 'LINEAR'
    e0 = ovr.color_ramp.elements[0]; e0.position = 0.30; e0.color = (0, 0, 0, 1)
    e1 = ovr.color_ramp.elements[1]; e1.position = 0.42; e1.color = (1, 1, 1, 1)
    e2 = ovr.color_ramp.elements.new(0.56); e2.color = (1, 1, 1, 1)
    e3 = ovr.color_ramp.elements.new(0.68); e3.color = (0, 0, 0, 1)
    nt.links.new(fr.outputs['Value'], ovr.inputs['Fac'])

    opn = n('ShaderNodeTexNoise'); opn.location = (-880, 900)       # ore only in patches
    opn.inputs['Scale'].default_value = 1.5
    opn.inputs['Detail'].default_value = 6.0
    nt.links.new(tc.outputs['Object'], opn.inputs['Vector'])
    opr = n('ShaderNodeValToRGB'); opr.location = (-660, 900)
    opr.color_ramp.elements[0].position = 0.26
    opr.color_ramp.elements[1].position = 0.50
    nt.links.new(opn.outputs['Fac'], opr.inputs['Fac'])

    om1 = n('ShaderNodeMath'); om1.operation = 'MULTIPLY'; om1.location = (-460, 760)
    nt.links.new(ovr.outputs['Color'], om1.inputs[0])
    nt.links.new(opr.outputs['Color'], om1.inputs[1])

    grn = n('ShaderNodeTexVoronoi'); grn.location = (-880, 1160)    # metallic grains / glints
    grn.inputs['Scale'].default_value = 55.0
    nt.links.new(tc.outputs['Object'], grn.inputs['Vector'])
    grr = n('ShaderNodeValToRGB'); grr.location = (-660, 1160)
    grr.color_ramp.elements[0].position = 0.00
    grr.color_ramp.elements[0].color = (1, 1, 1, 1)
    grr.color_ramp.elements[1].position = 0.22
    grr.color_ramp.elements[1].color = (0, 0, 0, 1)
    nt.links.new(grn.outputs['Distance'], grr.inputs['Fac'])
    gscale = n('ShaderNodeMath'); gscale.operation = 'MULTIPLY'; gscale.location = (-460, 1160)
    gscale.inputs[1].default_value = 0.38
    nt.links.new(grr.outputs['Color'], gscale.inputs[0])

    om2 = n('ShaderNodeMath'); om2.operation = 'ADD'; om2.location = (-260, 900)
    om2.use_clamp = True
    nt.links.new(om1.outputs['Value'], om2.inputs[0])
    nt.links.new(gscale.outputs['Value'], om2.inputs[1])

    oxi = n('ShaderNodeTexNoise'); oxi.location = (-460, 480)        # oxidised (dull) spots
    oxi.inputs['Scale'].default_value = 9.0
    oxi.inputs['Detail'].default_value = 6.0
    nt.links.new(tc.outputs['Object'], oxi.inputs['Vector'])
    oxr = n('ShaderNodeValToRGB'); oxr.location = (-260, 480)
    oxr.color_ramp.elements[0].position = 0.40
    oxr.color_ramp.elements[0].color = (0.05, 0.05, 0.05, 1)
    oxr.color_ramp.elements[1].position = 0.60
    oxr.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1)
    nt.links.new(oxi.outputs['Fac'], oxr.inputs['Fac'])
    ore_mask = n('ShaderNodeMath'); ore_mask.operation = 'MULTIPLY'
    ore_mask.location = (-60, 760); ore_mask.use_clamp = True
    ore_mask.name = "ORE_MASK"
    nt.links.new(om2.outputs['Value'], ore_mask.inputs[0])
    nt.links.new(oxr.outputs['Color'], ore_mask.inputs[1])

    # ---- ore colour: bronze metal, darker where oxidised --------------------
    ocn = n('ShaderNodeTexNoise'); ocn.location = (240, 620)
    ocn.inputs['Scale'].default_value = 12.0
    ocn.inputs['Detail'].default_value = 5.0
    nt.links.new(tc.outputs['Object'], ocn.inputs['Vector'])
    ocr = n('ShaderNodeValToRGB'); ocr.location = (440, 620)
    ocr.color_ramp.elements[0].position = 0.36
    ocr.color_ramp.elements[0].color = (0.175, 0.090, 0.036, 1)   # dark bronze
    ocr.color_ramp.elements[1].position = 0.64
    ocr.color_ramp.elements[1].color = (0.380, 0.212, 0.088, 1)   # bright bronze
    nt.links.new(ocn.outputs['Fac'], ocr.inputs['Fac'])

    base = n('ShaderNodeMixRGB'); base.blend_type = 'MIX'; base.location = (700, 180)
    base.name = "BASEMIX"
    nt.links.new(ore_mask.outputs['Value'], base.inputs['Fac'])
    nt.links.new(host.outputs['Color'], base.inputs['Color1'])
    nt.links.new(ocr.outputs['Color'], base.inputs['Color2'])
    # общий тон: темнее и теплее (иначе солнечная грань уходит в песочный)
    tint = n('ShaderNodeMixRGB'); tint.blend_type = 'MULTIPLY'; tint.location = (930, 180)
    tint.name = "BASECOLOR"
    tint.inputs['Fac'].default_value = 1.0
    tint.inputs['Color2'].default_value = (0.62, 0.55, 0.50, 1)
    nt.links.new(base.outputs['Color'], tint.inputs['Color1'])
    nt.links.new(tint.outputs['Color'], bsdf.inputs['Base Color'])

    # ---- metallic ----------------------------------------------------------
    met = n('ShaderNodeMath'); met.operation = 'MULTIPLY'; met.location = (700, -160)
    met.inputs[1].default_value = 0.72
    met.use_clamp = True
    met.name = "METALLIC"
    nt.links.new(ore_mask.outputs['Value'], met.inputs[0])
    nt.links.new(met.outputs['Value'], bsdf.inputs['Metallic'])

    # ---- roughness: rough stone, polished ore -------------------------------
    rgh = n('ShaderNodeTexNoise'); rgh.location = (240, -820)
    rgh.inputs['Scale'].default_value = 9.0
    rgh.inputs['Detail'].default_value = 4.0
    nt.links.new(tc.outputs['Object'], rgh.inputs['Vector'])
    rghr = n('ShaderNodeValToRGB'); rghr.location = (440, -820)
    rghr.color_ramp.elements[0].color = (0.88, 0.88, 0.88, 1)
    rghr.color_ramp.elements[1].color = (0.97, 0.97, 0.97, 1)
    nt.links.new(rgh.outputs['Fac'], rghr.inputs['Fac'])

    orgn = n('ShaderNodeTexNoise'); orgn.location = (240, -1060)
    orgn.inputs['Scale'].default_value = 26.0
    orgn.inputs['Detail'].default_value = 6.0
    nt.links.new(tc.outputs['Object'], orgn.inputs['Vector'])
    orgr = n('ShaderNodeValToRGB'); orgr.location = (440, -1060)
    orgr.color_ramp.elements[0].position = 0.36
    orgr.color_ramp.elements[0].color = (0.56, 0.56, 0.56, 1)     # shiny
    orgr.color_ramp.elements[1].position = 0.66
    orgr.color_ramp.elements[1].color = (0.86, 0.86, 0.86, 1)
    nt.links.new(orgn.outputs['Fac'], orgr.inputs['Fac'])

    rmix = n('ShaderNodeMixRGB'); rmix.blend_type = 'MIX'; rmix.location = (700, -560)
    rmix.name = "ROUGHNESS"
    nt.links.new(ore_mask.outputs['Value'], rmix.inputs['Fac'])
    nt.links.new(rghr.outputs['Color'], rmix.inputs['Color1'])
    nt.links.new(orgr.outputs['Color'], rmix.inputs['Color2'])
    nt.links.new(rmix.outputs['Color'], bsdf.inputs['Roughness'])

    # ---- micro bump --------------------------------------------------------
    b1 = n('ShaderNodeTexNoise'); b1.location = (240, -1400)
    b1.inputs['Scale'].default_value = 40.0
    b1.inputs['Detail'].default_value = 8.0
    b1.inputs['Roughness'].default_value = 0.68
    nt.links.new(tc.outputs['Object'], b1.inputs['Vector'])
    b2 = n('ShaderNodeTexVoronoi'); b2.location = (240, -1700)
    b2.feature = 'DISTANCE_TO_EDGE'
    b2.inputs['Scale'].default_value = 14.0
    nt.links.new(tc.outputs['Object'], b2.inputs['Vector'])
    b2r = n('ShaderNodeValToRGB'); b2r.location = (440, -1700)
    b2r.color_ramp.elements[0].position = 0.0
    b2r.color_ramp.elements[1].position = 0.07
    nt.links.new(b2.outputs['Distance'], b2r.inputs['Fac'])
    bmx = n('ShaderNodeMixRGB'); bmx.blend_type = 'MULTIPLY'; bmx.location = (700, -1500)
    bmx.inputs['Fac'].default_value = 0.65
    nt.links.new(b1.outputs['Fac'], bmx.inputs['Color1'])
    nt.links.new(b2r.outputs['Color'], bmx.inputs['Color2'])
    bump = n('ShaderNodeBump'); bump.location = (1100, -1100)
    bump.inputs['Strength'].default_value = 0.40
    bump.inputs['Distance'].default_value = 0.025
    nt.links.new(bmx.outputs['Color'], bump.inputs['Height'])
    nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])
    return mat

proc = iron_material()
for ob in (high, low):
    ob.data.materials.clear()
    ob.data.materials.append(proc)

# ---------------------------------------------------------------- bake ----
def new_img(name, is_data, fill):
    img = bpy.data.images.new(name, TEX_SIZE, TEX_SIZE, alpha=False, float_buffer=False, is_data=is_data)
    img.generated_color = fill
    return img

img_col = new_img(NAME + "_basecolor", False, (0.5, 0.5, 0.5, 1))
img_nrm = new_img(NAME + "_normal",    True,  (0.5, 0.5, 1.0, 1))
img_rgh = new_img(NAME + "_roughness", True,  (0.5, 0.5, 0.5, 1))
img_met = new_img(NAME + "_metallic",  True,  (0.0, 0.0, 0.0, 1))

sc.cycles.samples = 2
sc.cycles.use_denoising = False
sc.render.bake.margin = 24
sc.render.bake.use_pass_direct = False
sc.render.bake.use_pass_indirect = False
sc.render.bake.use_pass_color = True

def save(img):
    img.filepath_raw = os.path.join(OUT, img.name + ".png")
    img.file_format = 'PNG'
    img.save()

def bake_channel(node_name, img):
    """bake a raw shader value через Emission — работает и для metallic,
    и для базового цвета металла (DIFFUSE у металла даёт чёрный)."""
    m = proc.copy(); m.name = NAME + "_bake_" + node_name
    nt = m.node_tree
    src = nt.nodes[node_name]
    outn = [x for x in nt.nodes if x.type == 'OUTPUT_MATERIAL'][0]
    em = nt.nodes.new('ShaderNodeEmission')
    nt.links.new(src.outputs[0], em.inputs['Color'])
    nt.links.new(em.outputs['Emission'], outn.inputs['Surface'])
    tex = nt.nodes.new('ShaderNodeTexImage')
    tex.image = img
    nt.nodes.active = tex
    low.data.materials.clear(); low.data.materials.append(m)
    bpy.ops.object.select_all(action='DESELECT')
    low.select_set(True)
    bpy.context.view_layer.objects.active = low
    print(">>> baking EMIT", node_name, "->", img.name, flush=True)
    bpy.ops.object.bake(type='EMIT', use_clear=True)
    save(img)

sc.render.bake.use_selected_to_active = False
bake_channel("BASECOLOR", img_col)
bake_channel("ROUGHNESS", img_rgh)
bake_channel("METALLIC",  img_met)

# --- tangent-space normals: projected from the 82k highpoly
bake_mat = bpy.data.materials.new(NAME + "_bakenrm")
bake_mat.use_nodes = True
bnt = bake_mat.node_tree
tex_node = bnt.nodes.new('ShaderNodeTexImage')
tex_node.image = img_nrm
bnt.nodes.active = tex_node
low.data.materials.clear(); low.data.materials.append(bake_mat)
sc.render.bake.use_selected_to_active = True
sc.render.bake.cage_extrusion = 0.16
sc.render.bake.max_ray_distance = 0.40
bpy.ops.object.select_all(action='DESELECT')
high.select_set(True); low.select_set(True)
bpy.context.view_layer.objects.active = low
print(">>> baking NORMAL", flush=True)
bpy.ops.object.bake(type='NORMAL', use_clear=True, normal_space='TANGENT')
save(img_nrm)

# ------------------------------------------------ final PBR material ------
mat = bpy.data.materials.new("M_" + NAME)
mat.use_nodes = True
nt = mat.node_tree
nt.nodes.clear()
out  = nt.nodes.new('ShaderNodeOutputMaterial'); out.location = (600, 0)
bsdf = nt.nodes.new('ShaderNodeBsdfPrincipled'); bsdf.location = (300, 0)
nt.links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
tc = nt.nodes.new('ShaderNodeTexImage'); tc.image = img_col; tc.location = (-300, 400)
tr = nt.nodes.new('ShaderNodeTexImage'); tr.image = img_rgh; tr.location = (-300, 130)
tm = nt.nodes.new('ShaderNodeTexImage'); tm.image = img_met; tm.location = (-300, -140)
tn = nt.nodes.new('ShaderNodeTexImage'); tn.image = img_nrm; tn.location = (-300, -420)
for t in (tr, tm, tn):
    t.image.colorspace_settings.name = 'Non-Color'
nm = nt.nodes.new('ShaderNodeNormalMap'); nm.location = (10, -420)
nt.links.new(tc.outputs['Color'], bsdf.inputs['Base Color'])
nt.links.new(tr.outputs['Color'], bsdf.inputs['Roughness'])
nt.links.new(tm.outputs['Color'], bsdf.inputs['Metallic'])
nt.links.new(tn.outputs['Color'], nm.inputs['Color'])
nt.links.new(nm.outputs['Normal'], bsdf.inputs['Normal'])
low.data.materials.clear()
low.data.materials.append(mat)

high.hide_render = True
high.hide_viewport = True

# --------------------------------------------------------------- export ---
bpy.ops.object.select_all(action='DESELECT')
low.select_set(True)
bpy.context.view_layer.objects.active = low

bpy.ops.export_scene.gltf(filepath=os.path.join(OUT, NAME + ".glb"),
                          export_format='GLB', use_selection=True,
                          export_apply=True, export_yup=True)
try:
    bpy.ops.export_scene.fbx(filepath=os.path.join(OUT, NAME + ".fbx"),
                             use_selection=True, path_mode='COPY', embed_textures=True,
                             apply_scale_options='FBX_SCALE_ALL', mesh_smooth_type='FACE')
except Exception as e:
    print("FBX export failed:", e)
try:
    bpy.ops.wm.obj_export(filepath=os.path.join(OUT, NAME + ".obj"),
                          export_selected_objects=True, path_mode='COPY',
                          export_materials=True)
except Exception as e:
    print("OBJ export failed:", e)

bpy.ops.file.pack_all()
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT, NAME + ".blend"))

tris = sum(len(p.vertices) - 2 for p in low.data.polygons)
print("=== DONE ===")
print("lowpoly verts:", len(low.data.vertices), "tris:", tris)
print("dimensions m:", tuple(round(d, 3) for d in low.dimensions))
