""" The player's body, project side (module of the import runner, see import_assets.py). The body itself is the Paragon Twinblast character of the user's Fab pack (Content/ParagonTwinblast). THIS MODULE NEVER SAVES ANYTHING THERE: it only reads four of the pack's textures. It makes, under /Game/Player/Twinblast: M_Twinblast_LegsMetal the machine-leg material. UDescentPlayerBodyComponent puts it on the mesh component's lower-body slot (M04_LowerBody) at run time; the pack's own M_TwinBlast_LowerBody (leather, cloth, rubber) is untouched. What the material does. The leg GEOMETRY stays the pack's (a man's legs in trousers and boots); the SURFACE becomes plated steel: * the pack's normal map is mostly flattened where it shows leather (stitching, folds) and kept on the straps and bands; * plate seams are drawn from the REST-POSE position of the surface (pre-skinned, so they do not swim when he moves): rings every PlateHeight cm up the leg, and four upright seams per leg (front, back, inner, outer), neighbouring columns half a ring apart; a seam is a dark groove with a bright bevel beside it, and every plate has a slightly different steel tone; * the bands, straps and shoe patches (the pack's masks) are painted in the red of his machine arms (the pack's ArmColor). The pack's textures it reads: T_TwinBlast_LowerBody_MASK A = the large panels -> lighter steel; B = piping lines -> red T_TwinBlast_LowerBody_MASK2 A = bands, straps, shoe patches -> red T_TwinBlast_LowerBody_RGB G = edges -> bare metal; R = wear -> rougher; A = ambient occlusion T_TwinBlast_LowerBody__normals Safe to re-run: the graph is rebuilt every time (cheap). Every number is a parameter with its default here, so a look change is an edit of PARAMS below and `Tools\\import_assets.ps1 -Only twinblast`. """ import unreal GAME_DIR = "/Game/Player/Twinblast" PACK_TEX = "/Game/ParagonTwinblast/Characters/Heroes/TwinBlast/Textures/" MAT_NAME = "M_Twinblast_LegsMetal" # name: default. Colours are linear RGB; lengths are centimetres of the unscaled mesh. PARAMS = { "SteelColor": (0.090, 0.095, 0.106), # the body of the leg: gunmetal "PlateColor": (0.155, 0.162, 0.178), # the lighter plates (MASK.A) "RedColor": (0.470, 0.0136, 0.0046), # the pack's ArmColor "EdgeColor": (0.480, 0.490, 0.510), # bare metal: bevels and worn edges "EdgeStrength": 0.45, # of the pack's edge map (RGB.G) "BevelStrength": 0.55, "SteelMetallic": 0.55, "RedMetallic": 0.55, "SteelRoughness": 0.42, "RedRoughness": 0.30, "WearRoughness": 0.25, # added where RGB.R says worn "CreaseDarken": 0.75, # base colour in the deepest creases (AO = 0), 1 = no darkening "LeatherNormal": 0.22, # how much of the pack's normal map stays where it is not a strap or band "PlateHeight": 9.0, # cm between two ring seams "GrooveWidth": 0.32, # cm: half width of a seam's dark groove "GrooveDarken": 0.18, # base colour inside a groove "BevelAt": 0.75, # cm from the seam's middle to the bevel's highlight "BevelWidth": 0.40, "PlateTint": 0.30, # steel tone differs from plate to plate by up to this fraction } # The legs in the mesh's reference pose (mesh space: +Z up, +Y forward, +X his left; measured from the skeleton): # hip joint x 8.74, y -3.57, z 97.3; ankle x 17.9, y -9.16, z 12.5. So going down the leg its axis moves outward and back. LEG_TOP_Z = 97.0 LEG_OUT_PER_CM = 0.1085 # (17.9 - 8.74) / (97.3 - 12.5) LEG_BACK_PER_CM = 0.065 # (9.16 - 3.57) / (97.3 - 12.5) LEG_TOP_X = 8.74 LEG_TOP_Y = -3.6 def run(ctx): MEL, EAL = ctx.mel, ctx.eal EAL.make_directory(GAME_DIR) tex = {} for key, name in (("n", "T_TwinBlast_LowerBody__normals"), ("rgb", "T_TwinBlast_LowerBody_RGB"), ("mask", "T_TwinBlast_LowerBody_MASK"), ("mask2", "T_TwinBlast_LowerBody_MASK2")): tex[key] = EAL.load_asset(PACK_TEX + name) if tex[key] is None: raise RuntimeError("the pack texture %s%s is missing (Content/ParagonTwinblast)" % (PACK_TEX, name)) mat = ctx.get_or_create(GAME_DIR, MAT_NAME, unreal.Material, unreal.MaterialFactoryNew()) MEL.delete_all_material_expressions(mat) mat.set_editor_property("blend_mode", unreal.BlendMode.BLEND_OPAQUE) mat.set_editor_property("shading_model", unreal.MaterialShadingModel.MSM_DEFAULT_LIT) mat.set_editor_property("two_sided", False) cursor = {"y": -1400} def new(cls, x=-900): cursor["y"] += 70 return MEL.create_material_expression(mat, cls, x, cursor["y"]) def link(a, a_out, b, b_in): if not MEL.connect_material_expressions(a, a_out, b, b_in): raise RuntimeError("could not connect %s.%s -> %s.%s" % (type(a).__name__, a_out, type(b).__name__, b_in)) def sample(key, sampler): n = new(unreal.MaterialExpressionTextureSample, -1700) n.set_editor_property("texture", tex[key]) n.set_editor_property("sampler_type", sampler) return n def vec(name): n = new(unreal.MaterialExpressionVectorParameter, -1400) n.set_editor_property("parameter_name", name) c = PARAMS[name] n.set_editor_property("default_value", unreal.LinearColor(c[0], c[1], c[2], 1.0)) return n def scalar(name): n = new(unreal.MaterialExpressionScalarParameter, -1400) n.set_editor_property("parameter_name", name) n.set_editor_property("default_value", float(PARAMS[name])) return n def const(value): n = new(unreal.MaterialExpressionConstant, -1400) n.set_editor_property("r", float(value)) return n # (node, output name) pairs keep the wiring readable def out(node, name=""): return (node, name) def unary(cls, a): n = new(cls) link(a[0], a[1], n, "") return out(n) def binary(cls, a, b): """a and b are (node, output) pairs or plain numbers (the node's own constant).""" n = new(cls) for side, prop, pin in ((a, "const_a", "A"), (b, "const_b", "B")): if isinstance(side, tuple): link(side[0], side[1], n, pin) else: n.set_editor_property(prop, float(side)) return out(n) def add(a, b): return binary(unreal.MaterialExpressionAdd, a, b) def sub(a, b): return binary(unreal.MaterialExpressionSubtract, a, b) def mul(a, b): return binary(unreal.MaterialExpressionMultiply, a, b) def div(a, b): return binary(unreal.MaterialExpressionDivide, a, b) def vmin(a, b): return binary(unreal.MaterialExpressionMin, a, b) def saturate(a): return unary(unreal.MaterialExpressionSaturate, a) def one_minus(a): return unary(unreal.MaterialExpressionOneMinus, a) def vabs(a): return unary(unreal.MaterialExpressionAbs, a) def lerp(a, b, alpha): n = new(unreal.MaterialExpressionLinearInterpolate) link(a[0], a[1], n, "A") link(b[0], b[1], n, "B") link(alpha[0], alpha[1], n, "Alpha") return out(n) def channel(a, which): n = new(unreal.MaterialExpressionComponentMask) for c in "rgba": n.set_editor_property(c, c == which) link(a[0], a[1], n, "") return out(n) t_n = sample("n", unreal.MaterialSamplerType.SAMPLERTYPE_NORMAL) t_rgb = sample("rgb", unreal.MaterialSamplerType.SAMPLERTYPE_LINEAR_COLOR) t_mask = sample("mask", unreal.MaterialSamplerType.SAMPLERTYPE_COLOR) t_mask2 = sample("mask2", unreal.MaterialSamplerType.SAMPLERTYPE_COLOR) # ---- where the red goes: bands, straps and shoe patches (MASK2.A) plus the piping lines (MASK.B) ---- red_mask = saturate(add(out(t_mask2, "A"), out(t_mask, "B"))) not_red = one_minus(red_mask) # ---- the plates, from the rest-pose position ---- pos = unary(unreal.MaterialExpressionVertexInterpolator, out(new(unreal.MaterialExpressionPreSkinnedPosition, -1700))) px, py, pz = channel(pos, "r"), channel(pos, "g"), channel(pos, "b") down = sub(LEG_TOP_Z, pz) # cm below the hip side = add(add(py, mul(down, LEG_BACK_PER_CM)), -LEG_TOP_Y) # signed cm in front of the leg's axis across = sub(vabs(px), add(mul(down, LEG_OUT_PER_CM), LEG_TOP_X)) # signed cm outside the leg's axis quadrant = mul(unary(unreal.MaterialExpressionSign, side), unary(unreal.MaterialExpressionSign, across)) # -1 or +1 plate_h = out(scalar("PlateHeight")) rings = add(div(pz, plate_h), mul(quadrant, 0.25)) # neighbouring columns are half a ring apart ring_frac = unary(unreal.MaterialExpressionFrac, rings) to_ring = mul(vmin(ring_frac, one_minus(ring_frac)), plate_h) # cm to the nearest ring seam to_seam = vmin(to_ring, vmin(vabs(side), vabs(across))) # cm to the nearest seam of any kind groove = mul(one_minus(saturate(div(to_seam, out(scalar("GrooveWidth"))))), not_red) bevel = mul(saturate(one_minus(div(vabs(sub(to_seam, out(scalar("BevelAt")))), out(scalar("BevelWidth"))))), not_red) plate_id = add(unary(unreal.MaterialExpressionFloor, rings), mul(quadrant, 3.7)) plate_rand = unary(unreal.MaterialExpressionFrac, mul(unary(unreal.MaterialExpressionSine, mul(plate_id, 0.618)), 437.585)) plate_tint = add(1.0, mul(sub(plate_rand, 0.5), out(scalar("PlateTint")))) # ---- base colour ---- steel = mul(lerp(out(vec("SteelColor")), out(vec("PlateColor")), out(t_mask, "A")), plate_tint) base = lerp(steel, out(vec("RedColor")), red_mask) bright = saturate(add(mul(bevel, out(scalar("BevelStrength"))), mul(out(t_rgb, "G"), out(scalar("EdgeStrength"))))) base = lerp(base, out(vec("EdgeColor")), bright) base = mul(base, lerp(out(const(1.0)), out(scalar("GrooveDarken")), groove)) base = mul(base, lerp(out(scalar("CreaseDarken")), out(const(1.0)), out(t_rgb, "A"))) MEL.connect_material_property(base[0], base[1], unreal.MaterialProperty.MP_BASE_COLOR) # ---- metal, roughness ---- metallic = mul(lerp(out(scalar("SteelMetallic")), out(scalar("RedMetallic")), red_mask), sub(1.0, mul(groove, 0.7))) MEL.connect_material_property(metallic[0], metallic[1], unreal.MaterialProperty.MP_METALLIC) rough = lerp(out(scalar("SteelRoughness")), out(scalar("RedRoughness")), red_mask) rough = saturate(add(add(rough, mul(out(t_rgb, "R"), out(scalar("WearRoughness")))), mul(groove, 0.4))) MEL.connect_material_property(rough[0], rough[1], unreal.MaterialProperty.MP_ROUGHNESS) # ---- normal: the pack's relief on the straps and bands, nearly flat steel elsewhere ---- flat = new(unreal.MaterialExpressionConstant3Vector, -1400) flat.set_editor_property("constant", unreal.LinearColor(0.0, 0.0, 1.0, 1.0)) keep = lerp(out(scalar("LeatherNormal")), out(const(1.0)), red_mask) normal = unary(unreal.MaterialExpressionNormalize, lerp(out(flat), out(t_n, "RGB"), keep)) MEL.connect_material_property(normal[0], normal[1], unreal.MaterialProperty.MP_NORMAL) MEL.connect_material_property(t_rgb, "A", unreal.MaterialProperty.MP_AMBIENT_OCCLUSION) # It is drawn on a skeletal mesh: without this flag the engine falls back to the grey default material in a game. MEL.set_material_usage(mat, unreal.MaterialUsage.MATUSAGE_SKELETAL_MESH) MEL.recompile_material(mat) if not EAL.save_loaded_asset(mat): ctx.fail("could not save %s/%s (is an editor holding the file?)" % (GAME_DIR, MAT_NAME)) ctx.log("twinblast: %s/%s rebuilt (%d parameters; reads 4 pack textures, writes nothing in the pack)" % (GAME_DIR, MAT_NAME, len(PARAMS)))