Fix project structure
129
art-direction.md
@@ -203,6 +203,57 @@ Possible sections:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Card Dimensions and Resolution
|
||||||
|
|
||||||
|
### Aspect Ratio
|
||||||
|
|
||||||
|
All standard cards should use a **5:7 portrait aspect ratio**.
|
||||||
|
|
||||||
|
This corresponds cleanly with the current physical prototype dimensions:
|
||||||
|
|
||||||
|
- Width: 63 mm
|
||||||
|
- Height: 88.2 mm
|
||||||
|
|
||||||
|
Artwork, card frames, masks, material maps, and generated assets should
|
||||||
|
all use the same 5:7 coordinate space unless a specific experiment
|
||||||
|
requires otherwise.
|
||||||
|
|
||||||
|
### Master Artwork Resolution
|
||||||
|
|
||||||
|
The standard master artwork resolution should be:
|
||||||
|
|
||||||
|
**3000 × 4200 px**
|
||||||
|
|
||||||
|
This is the source-quality asset and should be retained even if smaller
|
||||||
|
versions are generated for the application.
|
||||||
|
|
||||||
|
All associated masks should use the same dimensions and alignment where
|
||||||
|
practical, including:
|
||||||
|
|
||||||
|
- Foil masks
|
||||||
|
- Holographic masks
|
||||||
|
- Metallic masks
|
||||||
|
- Roughness maps
|
||||||
|
- Wear masks
|
||||||
|
- Normal/bump maps
|
||||||
|
- Printing masks
|
||||||
|
|
||||||
|
### Runtime Resolutions
|
||||||
|
|
||||||
|
Runtime assets may be generated from the master depending on context:
|
||||||
|
|
||||||
|
- High-resolution inspection: 2000 × 2800
|
||||||
|
- Standard card rendering: 1000 × 1400
|
||||||
|
- Binder / collection thumbnails: 500 × 700 or smaller
|
||||||
|
|
||||||
|
Exact runtime sizes should eventually be determined through performance
|
||||||
|
testing, particularly on mobile devices.
|
||||||
|
|
||||||
|
The master artwork should never be replaced by a lower-resolution
|
||||||
|
runtime export.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Rarity Art Direction
|
# Rarity Art Direction
|
||||||
|
|
||||||
Rarity should have a major impact on the visual grammar of the card.
|
Rarity should have a major impact on the visual grammar of the card.
|
||||||
@@ -320,6 +371,7 @@ Uncommon cards can begin introducing more refinement.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## Rare
|
## Rare
|
||||||
|
|
||||||
Rare cards should feel distinctive.
|
Rare cards should feel distinctive.
|
||||||
@@ -446,9 +498,9 @@ These elements help tell David's story rather than simply adding decoration for
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Rarity vs. Finish
|
# Rarity vs. Physical Treatment
|
||||||
|
|
||||||
Rarity and finish are separate systems.
|
Rarity, material, and finish are separate systems.
|
||||||
|
|
||||||
This is important.
|
This is important.
|
||||||
|
|
||||||
@@ -465,24 +517,32 @@ A Legendary plain-paper card should still visually read as Legendary.
|
|||||||
- Base art complexity.
|
- Base art complexity.
|
||||||
- Ornamentation.
|
- Ornamentation.
|
||||||
|
|
||||||
|
## Material controls
|
||||||
|
|
||||||
|
- The physical substrate of the card.
|
||||||
|
- Surface microtexture and roughness.
|
||||||
|
- Edge and core appearance.
|
||||||
|
- Base reflectivity and perceived weight.
|
||||||
|
- Paper, linen, plastic, metal, or wood response.
|
||||||
|
|
||||||
## Finish controls
|
## Finish controls
|
||||||
|
|
||||||
- Physical rendering behavior.
|
- A treatment applied over the material and printed artwork.
|
||||||
- Reflectivity.
|
- Selective foil reflection.
|
||||||
- Foil effects.
|
- Holographic spectral response.
|
||||||
- Holographic behavior.
|
- Coating strength and coverage.
|
||||||
- Surface texture.
|
|
||||||
- Material response to lighting.
|
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
- Common Paper Timothy.
|
- Common Paper + Printed Ink Timothy.
|
||||||
- Common Foil Timothy.
|
- Common Paper + Foil Timothy.
|
||||||
- Common Holographic Timothy.
|
- Common Linen + Holographic Timothy.
|
||||||
- Common Metal Timothy.
|
- Common Metal + Printed Ink Timothy.
|
||||||
|
|
||||||
All of them should still use the same underlying Common Timothy card design.
|
All of them should still use the same underlying Common Timothy card design.
|
||||||
|
|
||||||
|
Material and finish should not be treated as an unrestricted Cartesian product. A combination should exist because it has an intentional physical and visual interpretation, not merely because both options are available independently.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Material and Finish Direction
|
# Material and Finish Direction
|
||||||
@@ -500,43 +560,68 @@ Examples:
|
|||||||
- Provenance -> inspectable metadata.
|
- Provenance -> inspectable metadata.
|
||||||
- Population -> inspectable metadata.
|
- Population -> inspectable metadata.
|
||||||
|
|
||||||
|
The material is the card's substrate. The finish is a treatment applied over that substrate. They should remain independently identifiable when the card moves under light.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Paper
|
## Paper
|
||||||
|
|
||||||
- Matte or slightly textured.
|
- Matte with fine, irregular fibers.
|
||||||
- Soft reflection.
|
- Broad, weak reflection rather than a sharp sheen.
|
||||||
- Visible paper grain if inspected closely.
|
- Grain should become visible during close inspection without making the artwork noisy.
|
||||||
|
|
||||||
## Plastic
|
## Plastic
|
||||||
|
|
||||||
- Smoother surface.
|
- Smoother surface.
|
||||||
- Slightly stronger specular response.
|
- Clear-coated dielectric reflection.
|
||||||
|
- A bounded moving highlight should remain visible even with Printed Ink.
|
||||||
- Cleaner edges.
|
- Cleaner edges.
|
||||||
|
|
||||||
## Linen
|
## Linen
|
||||||
|
|
||||||
- Fine woven normal map.
|
- Raised interlaced warp and weft rather than generic noise.
|
||||||
- Soft diffuse reflection.
|
- Soft diffuse reflection.
|
||||||
|
- Small shaded recesses should help the weave read under moving light.
|
||||||
- Particularly compatible with tapestry-inspired artwork.
|
- Particularly compatible with tapestry-inspired artwork.
|
||||||
|
|
||||||
## Metal
|
## Metal
|
||||||
|
|
||||||
- Strong metallic response.
|
- Reflective underprint and a visibly metallic edge.
|
||||||
- Sharper reflections.
|
- Darker artwork values create deeper engraved grooves; the maximum depth should remain low enough that the card still feels printed and collectible rather than sculpted.
|
||||||
|
- Groove depth should read through surface normals, restrained cavity shading, and fine bevel highlights when the card moves.
|
||||||
|
- Printed artwork colors must remain recognizable rather than becoming uniformly silver or gold.
|
||||||
- Visually heavier.
|
- Visually heavier.
|
||||||
- Should still preserve artwork readability.
|
- Should still preserve artwork readability.
|
||||||
|
|
||||||
|
## Wood
|
||||||
|
|
||||||
|
- Warm substrate tint with a darker brown core.
|
||||||
|
- Longitudinal, slightly warped grain with shallow physical relief.
|
||||||
|
- Grain should support the card artwork rather than overpowering it.
|
||||||
|
- Reflection should remain restrained and less polished than plastic or metal.
|
||||||
|
|
||||||
## Foil
|
## Foil
|
||||||
|
|
||||||
- Angle-dependent reflection.
|
- Selective angle-dependent metallic reflection controlled by an art-directed mask.
|
||||||
- Light sweeps visibly across the card.
|
- Light should sweep visibly across treated regions without washing out the entire illustration.
|
||||||
|
- Foil should remain distinguishable from a Metal substrate: foil is a localized surface treatment, while metal changes the whole card body and edge.
|
||||||
|
- Metal + Foil should preserve the printed colors and read as restrained, slightly raised champagne-metal polishing over etched steel. It should not turn the whole illustration into opaque gold.
|
||||||
|
|
||||||
## Holographic
|
## Holographic
|
||||||
|
|
||||||
- View-dependent spectral response.
|
- View-dependent spectral response.
|
||||||
- Holographic effect should ideally use masks so only specific portions of the artwork react.
|
- Holographic response should use masks so intentional portions of the artwork react.
|
||||||
- Should remain tasteful and avoid a neon / fantasy appearance.
|
- Should remain tasteful and avoid a neon / fantasy appearance.
|
||||||
|
- Spectral color can be clearly visible, but extreme white sheen at grazing angles should remain controlled.
|
||||||
|
|
||||||
|
## Finish Mask Direction
|
||||||
|
|
||||||
|
- Finish masks should be authored and reviewed as part of the card art rather than treated as arbitrary technical output.
|
||||||
|
- Preserve titles, verses, and other critical text unless a specific treatment is intentionally designed for them.
|
||||||
|
- Do not create obvious circular or elliptical exclusions around faces, hands, animals, or other subjects. Those shapes read as artificial holes when the card moves.
|
||||||
|
- Avoid isolated mask islands that produce unrelated holographic spots across skin or focal features.
|
||||||
|
- Prefer coverage that follows stained-glass panes, ornament, borders, garments, environmental color, and other compositional regions.
|
||||||
|
- Evaluate every mask while rotating the card under multiple lighting angles. A mask that looks acceptable in a still image may look distracting in motion.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
75
blender_prototype/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Sanctification TCG — Blender card material spike
|
||||||
|
|
||||||
|
Open `sanctification_card_material_prototype.blend` in Blender 5.2+.
|
||||||
|
|
||||||
|
The file uses one shared physical mesh (`GEO_Card_Master_63x88`) across every card instance. Each object overrides three material slots: front, universal back, and edge/core. The source art remains separate from geometry.
|
||||||
|
|
||||||
|
## Main organization
|
||||||
|
|
||||||
|
- `Cards/`: one collection per finish/substrate variant.
|
||||||
|
- `Scene_Setup/`: neutral studio, labels, four lights, and inspection cameras.
|
||||||
|
- `NG_CardSurfaceTexture`: preserves and lightly grades printed art.
|
||||||
|
- `NG_CardMicroNormal`: paper fiber, restrained linen weave, and scratch response.
|
||||||
|
- `NG_CardWear`: seeded surface/edge wear mask.
|
||||||
|
- `NG_CardFoil`: art-masked, view-responsive foil enhancement.
|
||||||
|
- `NG_CardHolographic`: broad, angle-dependent spectral shift with art-derived masking.
|
||||||
|
- `NG_CardBase`: shared Principled BSDF assembly.
|
||||||
|
|
||||||
|
The holographic animation is keyed on `CARD_Holo_Demo` from frames 1–120 at 24 fps.
|
||||||
|
|
||||||
|
### Premium finish revision (v3)
|
||||||
|
|
||||||
|
The three David finish materials now use a printed-ink BSDF mixed with a masked metallic reflection. Foil retains the artwork color in both layers: the old gold Screen blend, diffuse rainbow mix, thin-film color outlines, and high-frequency groove bump are absent from these materials. Holo colors the reflective layer using broad UV gradients and the signed view direction in card coordinates. It is an artistic diffraction approximation, not a spectral optical simulation.
|
||||||
|
|
||||||
|
`NG_David_PrintProtection` combines saturation/ink selection with soft UV exclusions for the face, hand, lamb, and central title panel. Those exclusions are specific to `legendary.png`; another artwork needs adjusted regions or an authored mask.
|
||||||
|
|
||||||
|
Edit `Finish Strength` on each premium material (foil 0.56, holo 0.60, metal experiment 0.72), the reflective BSDF Roughness (foil 0.19, holo 0.23), and `Broad diffraction spectrum` in the holo material. `CardRenderConfig.*` properties are descriptive snapshots; they do not drive shader values. `premium_finishes.py` owns these defaults and can update the existing scene without rebuilding geometry or the wear shader. The full builder calls it automatically.
|
||||||
|
|
||||||
|
Use `render_premium_review.py` in Blender to regenerate the review stills; set `REVIEW_ANIMATION=True` in its execution globals for the 120 PNG animation frames. The MP4 is encoded externally from `renders/holo_frames/holo_%04d.png` at 24 fps. Preview files with `material_revision` or `lighting_preview` in their names are historical experiments; the standard filenames are the current deliverables.
|
||||||
|
|
||||||
|
## Runtime translation
|
||||||
|
|
||||||
|
Reasonable bake/export candidates:
|
||||||
|
|
||||||
|
- Base color
|
||||||
|
- Roughness and metallic maps
|
||||||
|
- Paper/linen micro-normal
|
||||||
|
- Static wear and imperfection masks
|
||||||
|
|
||||||
|
Likely custom runtime shader work:
|
||||||
|
|
||||||
|
- View-dependent foil
|
||||||
|
- Holographic diffraction/spectral shift
|
||||||
|
- Angle response and moving reflection behavior
|
||||||
|
|
||||||
|
The Blender node setup is a look-development reference; it is not expected to survive glTF export unchanged.
|
||||||
|
|
||||||
|
## Delivered renders
|
||||||
|
|
||||||
|
- `comparison_lineup.png`
|
||||||
|
- `timothy_matte_closeup.png`
|
||||||
|
- `timothy_linen_closeup.png`
|
||||||
|
- `david_paper_closeup.png`
|
||||||
|
- `david_foil_closeup.png`
|
||||||
|
- `david_holographic_head_on.png`
|
||||||
|
- `david_holographic_spectrum_angle.png`
|
||||||
|
- `card_three_quarter_thickness.png`
|
||||||
|
- `wear_test_timothy.png`
|
||||||
|
- `universal_card_back.png`
|
||||||
|
- `holographic_rotation_demo.mp4` (5 seconds, 120 frames, 24 fps)
|
||||||
|
|
||||||
|
The animation is authored on the hidden `CARD_Holo_Demo` linked instance, not the lineup card. Its final range is −18° to +12°: wide enough to show the changing surface response while avoiding a strip-light angle that made the printed title unreadable. Blender in this environment did not expose FFmpeg output internally, so Blender rendered the PNG sequence and the included MP4 was encoded from those exact frames with H.264.
|
||||||
|
|
||||||
|
The studio uses 3 W key, 1 W fill, 0.65 W strip, and 1.4 W rim lights, a 0.012-strength world, and −1.35 stop AgX exposure. The narrower strip is positioned at (0.10, 0.012, 0.23) m to catch the demo's tilt range. These settings are shared by the saved scene, review stills, and animation.
|
||||||
|
|
||||||
|
## Spike observations
|
||||||
|
|
||||||
|
- Paper, linen, and laminate differ mainly through roughness, coat, and micro-normal; the art texture does not change.
|
||||||
|
- Linen remains intentionally close-inspection detail. Increasing `Linen Strength` beyond the current setting begins to compete with the watercolor/stained-glass texture.
|
||||||
|
- Foil is most convincing when the saturated/value-derived art mask controls the metallic lift instead of applying reflection uniformly.
|
||||||
|
- Holo needs a runtime view vector plus broad directional bands. Static glTF export can carry its base PBR state, but not the authored spectral shift.
|
||||||
|
- `David_MetalHolo` deliberately marks the excessive end of the range and is not a recommended product target.
|
||||||
|
|
||||||
|
## Rebuild
|
||||||
|
|
||||||
|
The scene was constructed through Blender MCP using `build_sanctification_card_prototype.py`. Running the script again intentionally rebuilds the current scene from scratch.
|
||||||
593
blender_prototype/build_sanctification_card_prototype.py
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = "/home/dkzver/dev/sanctification-tcg"
|
||||||
|
OUT = os.path.join(ROOT, "blender_prototype", "renders")
|
||||||
|
BLEND = os.path.join(ROOT, "blender_prototype", "sanctification_card_material_prototype.blend")
|
||||||
|
COMMON = os.path.join(ROOT, "common.png")
|
||||||
|
LEGENDARY = os.path.join(ROOT, "legendary.png")
|
||||||
|
BACK = os.path.join(ROOT, "card-back.png")
|
||||||
|
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_scene():
|
||||||
|
if bpy.context.object and bpy.context.object.mode != "OBJECT":
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
for block in (bpy.data.collections, bpy.data.materials, bpy.data.meshes,
|
||||||
|
bpy.data.curves, bpy.data.cameras, bpy.data.lights,
|
||||||
|
bpy.data.node_groups):
|
||||||
|
for item in list(block):
|
||||||
|
if item.users == 0:
|
||||||
|
block.remove(item)
|
||||||
|
|
||||||
|
|
||||||
|
def collection(name, parent=None):
|
||||||
|
c = bpy.data.collections.new(name)
|
||||||
|
(parent or bpy.context.scene.collection).children.link(c)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def link_only(obj, col):
|
||||||
|
for c in list(obj.users_collection):
|
||||||
|
c.objects.unlink(obj)
|
||||||
|
col.objects.link(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def sock(node, name, fallback=None):
|
||||||
|
s = node.inputs.get(name)
|
||||||
|
if s is None and fallback is not None:
|
||||||
|
s = node.inputs[fallback]
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def new_socket(group, name, in_out, socket_type, default=None, min_value=None, max_value=None):
|
||||||
|
s = group.interface.new_socket(name=name, in_out=in_out, socket_type=socket_type)
|
||||||
|
if default is not None:
|
||||||
|
s.default_value = default
|
||||||
|
if min_value is not None:
|
||||||
|
s.min_value = min_value
|
||||||
|
if max_value is not None:
|
||||||
|
s.max_value = max_value
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def make_surface_texture_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardSurfaceTexture", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Art Color", "INPUT", "NodeSocketColor", (0.8, 0.8, 0.8, 1))
|
||||||
|
new_socket(g, "Saturation", "INPUT", "NodeSocketFloat", 1.0, 0.0, 2.0)
|
||||||
|
new_socket(g, "Value", "INPUT", "NodeSocketFloat", 1.0, 0.0, 2.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
n_in = g.nodes.new("NodeGroupInput")
|
||||||
|
n_out = g.nodes.new("NodeGroupOutput")
|
||||||
|
hsv = g.nodes.new("ShaderNodeHueSaturation")
|
||||||
|
hsv.inputs[0].default_value = 0.5
|
||||||
|
hsv.inputs[3].default_value = 1.0
|
||||||
|
g.links.new(n_in.outputs["Art Color"], hsv.inputs[4])
|
||||||
|
g.links.new(n_in.outputs["Saturation"], hsv.inputs[1])
|
||||||
|
g.links.new(n_in.outputs["Value"], hsv.inputs[2])
|
||||||
|
g.links.new(hsv.outputs[0], n_out.inputs["Color"])
|
||||||
|
n_in.location = (-260, 0)
|
||||||
|
hsv.location = (0, 0)
|
||||||
|
n_out.location = (220, 0)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_wear_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardWear", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor", (0.8, 0.8, 0.8, 1))
|
||||||
|
new_socket(g, "Vector", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Wear Amount", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Edge Wear Amount", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Seed", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1000.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Wear Mask", "OUTPUT", "NodeSocketFloat")
|
||||||
|
|
||||||
|
ni = g.nodes.new("NodeGroupInput")
|
||||||
|
no = g.nodes.new("NodeGroupOutput")
|
||||||
|
sep = g.nodes.new("ShaderNodeSeparateXYZ")
|
||||||
|
subx = g.nodes.new("ShaderNodeMath"); subx.operation = "SUBTRACT"; subx.inputs[1].default_value = 0.5
|
||||||
|
suby = g.nodes.new("ShaderNodeMath"); suby.operation = "SUBTRACT"; suby.inputs[1].default_value = 0.5
|
||||||
|
absx = g.nodes.new("ShaderNodeMath"); absx.operation = "ABSOLUTE"
|
||||||
|
absy = g.nodes.new("ShaderNodeMath"); absy.operation = "ABSOLUTE"
|
||||||
|
maximum = g.nodes.new("ShaderNodeMath"); maximum.operation = "MAXIMUM"
|
||||||
|
edge = g.nodes.new("ShaderNodeMapRange")
|
||||||
|
edge.inputs[1].default_value = 0.435
|
||||||
|
edge.inputs[2].default_value = 0.5
|
||||||
|
edge.inputs[3].default_value = 0.0
|
||||||
|
edge.inputs[4].default_value = 1.0
|
||||||
|
edge.clamp = True
|
||||||
|
combine = g.nodes.new("ShaderNodeCombineXYZ")
|
||||||
|
seed_scale = g.nodes.new("ShaderNodeMath"); seed_scale.operation = "MULTIPLY"; seed_scale.inputs[1].default_value = 0.137
|
||||||
|
addvec = g.nodes.new("ShaderNodeVectorMath"); addvec.operation = "ADD"
|
||||||
|
noise = g.nodes.new("ShaderNodeTexNoise")
|
||||||
|
noise.inputs["Scale"].default_value = 22.0
|
||||||
|
noise.inputs["Detail"].default_value = 5.0
|
||||||
|
noise.inputs["Roughness"].default_value = 0.72
|
||||||
|
mul_edge_noise = g.nodes.new("ShaderNodeMath"); mul_edge_noise.operation = "MULTIPLY"
|
||||||
|
mul_edge_amt = g.nodes.new("ShaderNodeMath"); mul_edge_amt.operation = "MULTIPLY"
|
||||||
|
fine_noise = g.nodes.new("ShaderNodeTexNoise")
|
||||||
|
fine_noise.inputs["Scale"].default_value = 135.0
|
||||||
|
fine_noise.inputs["Detail"].default_value = 2.0
|
||||||
|
fine_ramp = g.nodes.new("ShaderNodeValToRGB")
|
||||||
|
fine_ramp.color_ramp.elements[0].position = 0.61
|
||||||
|
fine_ramp.color_ramp.elements[1].position = 0.73
|
||||||
|
mul_surface = g.nodes.new("ShaderNodeMath"); mul_surface.operation = "MULTIPLY"
|
||||||
|
add_masks = g.nodes.new("ShaderNodeMath"); add_masks.operation = "ADD"; add_masks.use_clamp = True
|
||||||
|
mix = g.nodes.new("ShaderNodeMixRGB")
|
||||||
|
mix.blend_type = "MIX"
|
||||||
|
mix.inputs[2].default_value = (0.72, 0.68, 0.57, 1)
|
||||||
|
|
||||||
|
g.links.new(ni.outputs["Vector"], sep.inputs[0])
|
||||||
|
g.links.new(sep.outputs[0], subx.inputs[0]); g.links.new(subx.outputs[0], absx.inputs[0])
|
||||||
|
g.links.new(sep.outputs[1], suby.inputs[0]); g.links.new(suby.outputs[0], absy.inputs[0])
|
||||||
|
g.links.new(absx.outputs[0], maximum.inputs[0]); g.links.new(absy.outputs[0], maximum.inputs[1])
|
||||||
|
g.links.new(maximum.outputs[0], edge.inputs[0])
|
||||||
|
g.links.new(ni.outputs["Seed"], seed_scale.inputs[0])
|
||||||
|
g.links.new(seed_scale.outputs[0], combine.inputs[0]); g.links.new(ni.outputs["Seed"], combine.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], addvec.inputs[0]); g.links.new(combine.outputs[0], addvec.inputs[1])
|
||||||
|
g.links.new(addvec.outputs[0], noise.inputs["Vector"]); g.links.new(addvec.outputs[0], fine_noise.inputs["Vector"])
|
||||||
|
g.links.new(edge.outputs[0], mul_edge_noise.inputs[0]); g.links.new(noise.outputs["Fac"], mul_edge_noise.inputs[1])
|
||||||
|
g.links.new(mul_edge_noise.outputs[0], mul_edge_amt.inputs[0]); g.links.new(ni.outputs["Edge Wear Amount"], mul_edge_amt.inputs[1])
|
||||||
|
g.links.new(fine_noise.outputs["Fac"], fine_ramp.inputs[0])
|
||||||
|
g.links.new(fine_ramp.outputs[0], mul_surface.inputs[0]); g.links.new(ni.outputs["Wear Amount"], mul_surface.inputs[1])
|
||||||
|
g.links.new(mul_edge_amt.outputs[0], add_masks.inputs[0]); g.links.new(mul_surface.outputs[0], add_masks.inputs[1])
|
||||||
|
g.links.new(add_masks.outputs[0], mix.inputs[0]); g.links.new(ni.outputs["Base Color"], mix.inputs[1])
|
||||||
|
g.links.new(mix.outputs[0], no.inputs["Color"]); g.links.new(add_masks.outputs[0], no.inputs["Wear Mask"])
|
||||||
|
for i, n in enumerate(g.nodes):
|
||||||
|
n.location = ((i % 6) * 180 - 520, -(i // 6) * 180)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_micro_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardMicroNormal", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Vector", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Micro Texture Strength", "INPUT", "NodeSocketFloat", 0.08, 0.0, 1.0)
|
||||||
|
new_socket(g, "Linen Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Holo Groove Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Scratch Amount", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Seed", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1000.0)
|
||||||
|
new_socket(g, "Normal", "OUTPUT", "NodeSocketVector")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
noise = g.nodes.new("ShaderNodeTexNoise")
|
||||||
|
noise.inputs["Scale"].default_value = 470.0
|
||||||
|
noise.inputs["Detail"].default_value = 2.0
|
||||||
|
noise.inputs["Roughness"].default_value = 0.62
|
||||||
|
wave_x = g.nodes.new("ShaderNodeTexWave"); wave_x.wave_type = "BANDS"; wave_x.bands_direction = "X"
|
||||||
|
wave_y = g.nodes.new("ShaderNodeTexWave"); wave_y.wave_type = "BANDS"; wave_y.bands_direction = "Y"
|
||||||
|
for w in (wave_x, wave_y):
|
||||||
|
w.inputs["Scale"].default_value = 190.0
|
||||||
|
w.inputs["Distortion"].default_value = 0.8
|
||||||
|
w.inputs["Detail"].default_value = 2.0
|
||||||
|
weave = g.nodes.new("ShaderNodeMath"); weave.operation = "ADD"
|
||||||
|
weave_amt = g.nodes.new("ShaderNodeMath"); weave_amt.operation = "MULTIPLY"
|
||||||
|
paper_amt = g.nodes.new("ShaderNodeMath"); paper_amt.operation = "MULTIPLY"
|
||||||
|
add = g.nodes.new("ShaderNodeMath"); add.operation = "ADD"
|
||||||
|
scratch_wave = g.nodes.new("ShaderNodeTexWave"); scratch_wave.wave_type = "BANDS"; scratch_wave.bands_direction = "X"
|
||||||
|
scratch_wave.inputs["Scale"].default_value = 1250.0
|
||||||
|
scratch_wave.inputs["Distortion"].default_value = 8.0
|
||||||
|
scratch_ramp = g.nodes.new("ShaderNodeValToRGB")
|
||||||
|
scratch_ramp.color_ramp.elements[0].position = 0.48
|
||||||
|
scratch_ramp.color_ramp.elements[1].position = 0.505
|
||||||
|
scratch_amt = g.nodes.new("ShaderNodeMath"); scratch_amt.operation = "MULTIPLY"
|
||||||
|
add2 = g.nodes.new("ShaderNodeMath"); add2.operation = "ADD"
|
||||||
|
groove_wave = g.nodes.new("ShaderNodeTexWave"); groove_wave.wave_type = "BANDS"; groove_wave.bands_direction = "X"
|
||||||
|
groove_wave.inputs["Scale"].default_value = 900.0
|
||||||
|
groove_wave.inputs["Distortion"].default_value = 1.8
|
||||||
|
groove_wave.inputs["Detail"].default_value = 2.0
|
||||||
|
groove_amt = g.nodes.new("ShaderNodeMath"); groove_amt.operation = "MULTIPLY"
|
||||||
|
add3 = g.nodes.new("ShaderNodeMath"); add3.operation = "ADD"
|
||||||
|
bump = g.nodes.new("ShaderNodeBump")
|
||||||
|
bump.inputs["Distance"].default_value = 0.00015
|
||||||
|
bump.inputs["Strength"].default_value = 0.42
|
||||||
|
g.links.new(ni.outputs["Vector"], noise.inputs["Vector"])
|
||||||
|
g.links.new(ni.outputs["Vector"], wave_x.inputs["Vector"]); g.links.new(ni.outputs["Vector"], wave_y.inputs["Vector"])
|
||||||
|
g.links.new(wave_x.outputs["Color"], weave.inputs[0]); g.links.new(wave_y.outputs["Color"], weave.inputs[1])
|
||||||
|
g.links.new(weave.outputs[0], weave_amt.inputs[0]); g.links.new(ni.outputs["Linen Strength"], weave_amt.inputs[1])
|
||||||
|
g.links.new(noise.outputs["Fac"], paper_amt.inputs[0]); g.links.new(ni.outputs["Micro Texture Strength"], paper_amt.inputs[1])
|
||||||
|
g.links.new(weave_amt.outputs[0], add.inputs[0]); g.links.new(paper_amt.outputs[0], add.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], scratch_wave.inputs["Vector"]); g.links.new(scratch_wave.outputs["Color"], scratch_ramp.inputs[0])
|
||||||
|
g.links.new(scratch_ramp.outputs[0], scratch_amt.inputs[0]); g.links.new(ni.outputs["Scratch Amount"], scratch_amt.inputs[1])
|
||||||
|
g.links.new(add.outputs[0], add2.inputs[0]); g.links.new(scratch_amt.outputs[0], add2.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], groove_wave.inputs["Vector"]); g.links.new(groove_wave.outputs["Color"], groove_amt.inputs[0]); g.links.new(ni.outputs["Holo Groove Strength"], groove_amt.inputs[1])
|
||||||
|
g.links.new(add2.outputs[0], add3.inputs[0]); g.links.new(groove_amt.outputs[0], add3.inputs[1])
|
||||||
|
g.links.new(add3.outputs[0], bump.inputs["Height"]); g.links.new(bump.outputs["Normal"], no.inputs["Normal"])
|
||||||
|
for i, n in enumerate(g.nodes): n.location = ((i % 5) * 190 - 450, -(i // 5) * 190)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_foil_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardFoil", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Art Mask", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Metallic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Roughness", "INPUT", "NodeSocketFloat", 0.4, 0.0, 1.0)
|
||||||
|
new_socket(g, "Foil Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Foil Roughness", "INPUT", "NodeSocketFloat", 0.22, 0.0, 1.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Metallic", "OUTPUT", "NodeSocketFloat")
|
||||||
|
new_socket(g, "Roughness", "OUTPUT", "NodeSocketFloat")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
lw = g.nodes.new("ShaderNodeLayerWeight")
|
||||||
|
inv = g.nodes.new("ShaderNodeMath"); inv.operation = "SUBTRACT"; inv.inputs[0].default_value = 1.0
|
||||||
|
angle = g.nodes.new("ShaderNodeMath"); angle.operation = "MULTIPLY_ADD"; angle.inputs[1].default_value = 0.65; angle.inputs[2].default_value = 0.18
|
||||||
|
mask = g.nodes.new("ShaderNodeMath"); mask.operation = "MULTIPLY"
|
||||||
|
amt = g.nodes.new("ShaderNodeMath"); amt.operation = "MULTIPLY"
|
||||||
|
static_amt = g.nodes.new("ShaderNodeMath"); static_amt.operation = "MULTIPLY"
|
||||||
|
mix = g.nodes.new("ShaderNodeMixRGB"); mix.blend_type = "SCREEN"; mix.inputs[2].default_value = (0.74, 0.67, 0.48, 1)
|
||||||
|
metboost = g.nodes.new("ShaderNodeMath"); metboost.operation = "MULTIPLY"; metboost.inputs[1].default_value = 0.72
|
||||||
|
metadd = g.nodes.new("ShaderNodeMath"); metadd.operation = "ADD"; metadd.use_clamp = True
|
||||||
|
roughmix = g.nodes.new("ShaderNodeMix")
|
||||||
|
roughmix.data_type = "FLOAT"
|
||||||
|
g.links.new(lw.outputs["Facing"], inv.inputs[1]); g.links.new(lw.outputs["Facing"], angle.inputs[0])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], mask.inputs[0]); g.links.new(angle.outputs[0], mask.inputs[1])
|
||||||
|
g.links.new(mask.outputs[0], amt.inputs[0]); g.links.new(ni.outputs["Foil Strength"], amt.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], static_amt.inputs[0]); g.links.new(ni.outputs["Foil Strength"], static_amt.inputs[1])
|
||||||
|
g.links.new(amt.outputs[0], mix.inputs[0]); g.links.new(ni.outputs["Base Color"], mix.inputs[1])
|
||||||
|
g.links.new(static_amt.outputs[0], metboost.inputs[0]); g.links.new(metboost.outputs[0], metadd.inputs[0]); g.links.new(ni.outputs["Base Metallic"], metadd.inputs[1])
|
||||||
|
g.links.new(static_amt.outputs[0], roughmix.inputs[0]); g.links.new(ni.outputs["Base Roughness"], roughmix.inputs[2]); g.links.new(ni.outputs["Foil Roughness"], roughmix.inputs[3])
|
||||||
|
g.links.new(mix.outputs[0], no.inputs["Color"]); g.links.new(metadd.outputs[0], no.inputs["Metallic"]); g.links.new(roughmix.outputs[0], no.inputs["Roughness"])
|
||||||
|
for i, n in enumerate(g.nodes): n.location = ((i % 5) * 190 - 420, -(i // 5) * 190)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_holo_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardHolographic", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Vector", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Art Mask", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Metallic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Roughness", "INPUT", "NodeSocketFloat", 0.4, 0.0, 1.0)
|
||||||
|
new_socket(g, "Holo Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.5)
|
||||||
|
new_socket(g, "Holo Scale", "INPUT", "NodeSocketFloat", 5.0, 0.5, 30.0)
|
||||||
|
new_socket(g, "Holo Saturation", "INPUT", "NodeSocketFloat", 0.72, 0.0, 1.5)
|
||||||
|
new_socket(g, "Holo Angle Response", "INPUT", "NodeSocketFloat", 0.85, 0.0, 2.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Metallic", "OUTPUT", "NodeSocketFloat")
|
||||||
|
new_socket(g, "Roughness", "OUTPUT", "NodeSocketFloat")
|
||||||
|
new_socket(g, "Thin Film Thickness", "OUTPUT", "NodeSocketFloat")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
lw = g.nodes.new("ShaderNodeLayerWeight")
|
||||||
|
inv = g.nodes.new("ShaderNodeMath"); inv.operation = "SUBTRACT"; inv.inputs[0].default_value = 1.0
|
||||||
|
power = g.nodes.new("ShaderNodeMath"); power.operation = "POWER"; power.inputs[1].default_value = 0.38
|
||||||
|
angle = g.nodes.new("ShaderNodeMath"); angle.operation = "MULTIPLY"
|
||||||
|
anglebase = g.nodes.new("ShaderNodeMath"); anglebase.operation = "ADD"; anglebase.inputs[1].default_value = 0.035
|
||||||
|
wave = g.nodes.new("ShaderNodeTexWave"); wave.wave_type = "BANDS"; wave.bands_direction = "X"
|
||||||
|
wave.inputs["Distortion"].default_value = 2.2; wave.inputs["Detail"].default_value = 3.0
|
||||||
|
rotate = g.nodes.new("ShaderNodeVectorRotate"); rotate.rotation_type = "AXIS_ANGLE"; rotate.inputs["Axis"].default_value = (0,0,1); rotate.inputs["Angle"].default_value = math.radians(33)
|
||||||
|
wave2 = g.nodes.new("ShaderNodeTexWave"); wave2.wave_type = "BANDS"; wave2.bands_direction = "X"; wave2.inputs["Distortion"].default_value = 1.1; wave2.inputs["Detail"].default_value = 2.0
|
||||||
|
scale2 = g.nodes.new("ShaderNodeMath"); scale2.operation = "MULTIPLY"; scale2.inputs[1].default_value = 0.63
|
||||||
|
wave1_amt = g.nodes.new("ShaderNodeMath"); wave1_amt.operation = "MULTIPLY"; wave1_amt.inputs[1].default_value = 0.72
|
||||||
|
wave2_amt = g.nodes.new("ShaderNodeMath"); wave2_amt.operation = "MULTIPLY"; wave2_amt.inputs[1].default_value = 0.28
|
||||||
|
waves_add = g.nodes.new("ShaderNodeMath"); waves_add.operation = "ADD"
|
||||||
|
noise = g.nodes.new("ShaderNodeTexNoise"); noise.inputs["Scale"].default_value = 3.2; noise.inputs["Detail"].default_value = 2.0
|
||||||
|
nscale = g.nodes.new("ShaderNodeMath"); nscale.operation = "MULTIPLY"; nscale.inputs[1].default_value = 0.16
|
||||||
|
fscale = g.nodes.new("ShaderNodeMath"); fscale.operation = "MULTIPLY"; fscale.inputs[1].default_value = 0.72
|
||||||
|
add1 = g.nodes.new("ShaderNodeMath"); add1.operation = "ADD"
|
||||||
|
add2 = g.nodes.new("ShaderNodeMath"); add2.operation = "ADD"
|
||||||
|
fract = g.nodes.new("ShaderNodeMath"); fract.operation = "FRACT"
|
||||||
|
ramp = g.nodes.new("ShaderNodeValToRGB")
|
||||||
|
cr = ramp.color_ramp
|
||||||
|
cr.interpolation = "EASE"
|
||||||
|
while len(cr.elements) > 2: cr.elements.remove(cr.elements[-1])
|
||||||
|
cr.elements[0].position = 0.0; cr.elements[0].color = (1.0, 0.34, 0.28, 1)
|
||||||
|
cr.elements[1].position = 1.0; cr.elements[1].color = (1.0, 0.34, 0.28, 1)
|
||||||
|
for pos, color in [
|
||||||
|
(0.17, (1.0, 0.76, 0.24, 1)), (0.34, (0.28, 0.95, 0.58, 1)),
|
||||||
|
(0.51, (0.20, 0.78, 1.0, 1)), (0.68, (0.38, 0.48, 1.0, 1)),
|
||||||
|
(0.85, (0.95, 0.32, 0.82, 1))]:
|
||||||
|
e = cr.elements.new(pos); e.color = color
|
||||||
|
hsv = g.nodes.new("ShaderNodeHueSaturation"); hsv.inputs[0].default_value = 0.5; hsv.inputs[2].default_value = 1.0; hsv.inputs[3].default_value = 1.0
|
||||||
|
holoamt = g.nodes.new("ShaderNodeMath"); holoamt.operation = "MULTIPLY"
|
||||||
|
holoamt2 = g.nodes.new("ShaderNodeMath"); holoamt2.operation = "MULTIPLY"
|
||||||
|
holo_cap = g.nodes.new("ShaderNodeMath"); holo_cap.operation = "MINIMUM"; holo_cap.inputs[1].default_value = 0.62
|
||||||
|
mix = g.nodes.new("ShaderNodeMixRGB"); mix.blend_type = "MIX"
|
||||||
|
metboost = g.nodes.new("ShaderNodeMath"); metboost.operation = "MULTIPLY"; metboost.inputs[1].default_value = 0.42
|
||||||
|
metadd = g.nodes.new("ShaderNodeMath"); metadd.operation = "ADD"; metadd.use_clamp = True
|
||||||
|
rough_target = g.nodes.new("ShaderNodeMath"); rough_target.operation = "MULTIPLY"; rough_target.inputs[1].default_value = 0.62
|
||||||
|
roughmix = g.nodes.new("ShaderNodeMix"); roughmix.data_type = "FLOAT"
|
||||||
|
film_mask = g.nodes.new("ShaderNodeMath"); film_mask.operation = "MULTIPLY"
|
||||||
|
film_scale = g.nodes.new("ShaderNodeMath"); film_scale.operation = "MULTIPLY"; film_scale.inputs[1].default_value = 460.0
|
||||||
|
g.links.new(lw.outputs["Facing"], inv.inputs[1]); g.links.new(lw.outputs["Facing"], power.inputs[0]); g.links.new(power.outputs[0], angle.inputs[0]); g.links.new(ni.outputs["Holo Angle Response"], angle.inputs[1]); g.links.new(angle.outputs[0], anglebase.inputs[0])
|
||||||
|
g.links.new(ni.outputs["Vector"], wave.inputs["Vector"]); g.links.new(ni.outputs["Holo Scale"], wave.inputs["Scale"])
|
||||||
|
g.links.new(ni.outputs["Vector"], rotate.inputs["Vector"]); g.links.new(rotate.outputs["Vector"], wave2.inputs["Vector"]); g.links.new(ni.outputs["Holo Scale"], scale2.inputs[0]); g.links.new(scale2.outputs[0], wave2.inputs["Scale"])
|
||||||
|
g.links.new(wave.outputs["Fac"], wave1_amt.inputs[0]); g.links.new(wave2.outputs["Fac"], wave2_amt.inputs[0]); g.links.new(wave1_amt.outputs[0], waves_add.inputs[0]); g.links.new(wave2_amt.outputs[0], waves_add.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], noise.inputs["Vector"]); g.links.new(noise.outputs["Fac"], nscale.inputs[0]); g.links.new(lw.outputs["Facing"], fscale.inputs[0])
|
||||||
|
g.links.new(waves_add.outputs[0], add1.inputs[0]); g.links.new(nscale.outputs[0], add1.inputs[1]); g.links.new(add1.outputs[0], add2.inputs[0]); g.links.new(fscale.outputs[0], add2.inputs[1]); g.links.new(add2.outputs[0], fract.inputs[0]); g.links.new(fract.outputs[0], ramp.inputs[0]); g.links.new(ramp.outputs[0], hsv.inputs[4]); g.links.new(ni.outputs["Holo Saturation"], hsv.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], holoamt.inputs[0]); g.links.new(anglebase.outputs[0], holoamt.inputs[1]); g.links.new(holoamt.outputs[0], holoamt2.inputs[0]); g.links.new(ni.outputs["Holo Strength"], holoamt2.inputs[1])
|
||||||
|
g.links.new(holoamt2.outputs[0], holo_cap.inputs[0]); g.links.new(holo_cap.outputs[0], mix.inputs[0]); g.links.new(ni.outputs["Base Color"], mix.inputs[1]); g.links.new(hsv.outputs[0], mix.inputs[2])
|
||||||
|
g.links.new(holo_cap.outputs[0], metboost.inputs[0]); g.links.new(metboost.outputs[0], metadd.inputs[0]); g.links.new(ni.outputs["Base Metallic"], metadd.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Base Roughness"], rough_target.inputs[0]); g.links.new(holo_cap.outputs[0], roughmix.inputs[0]); g.links.new(ni.outputs["Base Roughness"], roughmix.inputs[2]); g.links.new(rough_target.outputs[0], roughmix.inputs[3])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], film_mask.inputs[0]); g.links.new(ni.outputs["Holo Strength"], film_mask.inputs[1]); g.links.new(film_mask.outputs[0], film_scale.inputs[0])
|
||||||
|
g.links.new(mix.outputs[0], no.inputs["Color"]); g.links.new(metadd.outputs[0], no.inputs["Metallic"]); g.links.new(roughmix.outputs[0], no.inputs["Roughness"]); g.links.new(film_scale.outputs[0], no.inputs["Thin Film Thickness"])
|
||||||
|
for i, n in enumerate(g.nodes): n.location = ((i % 6) * 190 - 520, -(i // 6) * 190)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_base_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardBase", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Roughness", "INPUT", "NodeSocketFloat", 0.45, 0.0, 1.0)
|
||||||
|
new_socket(g, "Metallic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Coat Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Coat Roughness", "INPUT", "NodeSocketFloat", 0.18, 0.0, 1.0)
|
||||||
|
new_socket(g, "Anisotropic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Sheen Weight", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Thin Film Thickness", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1000.0)
|
||||||
|
new_socket(g, "Thin Film IOR", "INPUT", "NodeSocketFloat", 1.38, 1.0, 3.0)
|
||||||
|
new_socket(g, "Normal", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Shader", "OUTPUT", "NodeSocketShader")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
bsdf = g.nodes.new("ShaderNodeBsdfPrincipled")
|
||||||
|
g.links.new(ni.outputs["Base Color"], sock(bsdf, "Base Color"))
|
||||||
|
g.links.new(ni.outputs["Roughness"], sock(bsdf, "Roughness"))
|
||||||
|
g.links.new(ni.outputs["Metallic"], sock(bsdf, "Metallic"))
|
||||||
|
g.links.new(ni.outputs["Coat Strength"], sock(bsdf, "Coat Weight", 18))
|
||||||
|
g.links.new(ni.outputs["Coat Roughness"], sock(bsdf, "Coat Roughness", 19))
|
||||||
|
g.links.new(ni.outputs["Anisotropic"], bsdf.inputs["Anisotropic"])
|
||||||
|
g.links.new(ni.outputs["Sheen Weight"], bsdf.inputs["Sheen Weight"])
|
||||||
|
g.links.new(ni.outputs["Thin Film Thickness"], bsdf.inputs["Thin Film Thickness"])
|
||||||
|
g.links.new(ni.outputs["Thin Film IOR"], bsdf.inputs["Thin Film IOR"])
|
||||||
|
g.links.new(ni.outputs["Normal"], sock(bsdf, "Normal"))
|
||||||
|
g.links.new(bsdf.outputs["BSDF"], no.inputs["Shader"])
|
||||||
|
ni.location = (-260, 0); bsdf.location = (0, 0); no.location = (260, 0)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def load_image(path, name):
|
||||||
|
img = bpy.data.images.load(path, check_existing=True)
|
||||||
|
img.name = name
|
||||||
|
img.colorspace_settings.name = "sRGB"
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def create_front_material(name, image, cfg, groups):
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
mat.use_nodes = True
|
||||||
|
mat.diffuse_color = (0.35, 0.3, 0.24, 1)
|
||||||
|
nt = mat.node_tree; nt.nodes.clear()
|
||||||
|
out = nt.nodes.new("ShaderNodeOutputMaterial"); out.location = (1050, 0)
|
||||||
|
texcoord = nt.nodes.new("ShaderNodeTexCoord"); texcoord.location = (-1100, -180)
|
||||||
|
tex = nt.nodes.new("ShaderNodeTexImage"); tex.image = image; tex.interpolation = "Linear"; tex.location = (-1100, 180); tex.label = image.name
|
||||||
|
surf = nt.nodes.new("ShaderNodeGroup"); surf.node_tree = groups["surface"]; surf.location = (-820, 180)
|
||||||
|
wear = nt.nodes.new("ShaderNodeGroup"); wear.node_tree = groups["wear"]; wear.location = (-570, 180)
|
||||||
|
sep = nt.nodes.new("ShaderNodeSeparateColor"); sep.mode = "HSV"; sep.location = (-810, -80)
|
||||||
|
maskmul = nt.nodes.new("ShaderNodeMath"); maskmul.operation = "MULTIPLY"; maskmul.location = (-575, -80)
|
||||||
|
maskramp = nt.nodes.new("ShaderNodeValToRGB"); maskramp.location = (-390, -80)
|
||||||
|
maskramp.color_ramp.elements[0].position = 0.20
|
||||||
|
maskramp.color_ramp.elements[1].position = 0.56
|
||||||
|
foil = nt.nodes.new("ShaderNodeGroup"); foil.node_tree = groups["foil"]; foil.location = (-105, 170)
|
||||||
|
holo = nt.nodes.new("ShaderNodeGroup"); holo.node_tree = groups["holo"]; holo.location = (170, 170)
|
||||||
|
micro = nt.nodes.new("ShaderNodeGroup"); micro.node_tree = groups["micro"]; micro.location = (170, -240)
|
||||||
|
base = nt.nodes.new("ShaderNodeGroup"); base.node_tree = groups["base"]; base.location = (640, 80)
|
||||||
|
surf.inputs["Saturation"].default_value = cfg.get("saturation", 1.0)
|
||||||
|
surf.inputs["Value"].default_value = cfg.get("value", 1.0)
|
||||||
|
wear.inputs["Wear Amount"].default_value = cfg.get("wear", 0.0)
|
||||||
|
wear.inputs["Edge Wear Amount"].default_value = cfg.get("edge_wear", 0.0)
|
||||||
|
wear.inputs["Seed"].default_value = cfg.get("seed", 1.0)
|
||||||
|
foil.inputs["Base Metallic"].default_value = cfg.get("metallic", 0.0)
|
||||||
|
foil.inputs["Base Roughness"].default_value = cfg.get("roughness", 0.5)
|
||||||
|
foil.inputs["Foil Strength"].default_value = cfg.get("foil", 0.0)
|
||||||
|
foil.inputs["Foil Roughness"].default_value = cfg.get("foil_roughness", 0.22)
|
||||||
|
holo.inputs["Holo Strength"].default_value = cfg.get("holo", 0.0)
|
||||||
|
holo.inputs["Holo Scale"].default_value = cfg.get("holo_scale", 5.0)
|
||||||
|
holo.inputs["Holo Saturation"].default_value = cfg.get("holo_saturation", 0.72)
|
||||||
|
holo.inputs["Holo Angle Response"].default_value = cfg.get("holo_angle", 0.85)
|
||||||
|
micro.inputs["Micro Texture Strength"].default_value = cfg.get("micro", 0.06)
|
||||||
|
micro.inputs["Linen Strength"].default_value = cfg.get("linen", 0.0)
|
||||||
|
micro.inputs["Holo Groove Strength"].default_value = cfg.get("holo_groove", 0.0)
|
||||||
|
micro.inputs["Scratch Amount"].default_value = cfg.get("scratches", 0.0)
|
||||||
|
micro.inputs["Seed"].default_value = cfg.get("seed", 1.0)
|
||||||
|
base.inputs["Coat Strength"].default_value = cfg.get("coat", 0.0)
|
||||||
|
base.inputs["Coat Roughness"].default_value = cfg.get("coat_roughness", 0.2)
|
||||||
|
base.inputs["Anisotropic"].default_value = cfg.get("anisotropic", 0.0)
|
||||||
|
base.inputs["Sheen Weight"].default_value = cfg.get("sheen", 0.0)
|
||||||
|
base.inputs["Thin Film IOR"].default_value = cfg.get("thin_film_ior", 1.38)
|
||||||
|
nt.links.new(texcoord.outputs["UV"], tex.inputs["Vector"]); nt.links.new(tex.outputs["Color"], surf.inputs["Art Color"])
|
||||||
|
nt.links.new(surf.outputs["Color"], wear.inputs["Base Color"]); nt.links.new(texcoord.outputs["Generated"], wear.inputs["Vector"])
|
||||||
|
nt.links.new(surf.outputs["Color"], sep.inputs["Color"]); nt.links.new(sep.outputs[1], maskmul.inputs[0]); nt.links.new(sep.outputs[2], maskmul.inputs[1]); nt.links.new(maskmul.outputs[0], maskramp.inputs[0])
|
||||||
|
nt.links.new(wear.outputs["Color"], foil.inputs["Base Color"]); nt.links.new(maskramp.outputs["Color"], foil.inputs["Art Mask"])
|
||||||
|
nt.links.new(foil.outputs["Color"], holo.inputs["Base Color"]); nt.links.new(texcoord.outputs["Generated"], holo.inputs["Vector"]); nt.links.new(maskramp.outputs["Color"], holo.inputs["Art Mask"]); nt.links.new(foil.outputs["Metallic"], holo.inputs["Base Metallic"]); nt.links.new(foil.outputs["Roughness"], holo.inputs["Base Roughness"])
|
||||||
|
nt.links.new(texcoord.outputs["Generated"], micro.inputs["Vector"])
|
||||||
|
nt.links.new(holo.outputs["Color"], base.inputs["Base Color"]); nt.links.new(holo.outputs["Metallic"], base.inputs["Metallic"]); nt.links.new(holo.outputs["Roughness"], base.inputs["Roughness"]); nt.links.new(holo.outputs["Thin Film Thickness"], base.inputs["Thin Film Thickness"]); nt.links.new(micro.outputs["Normal"], base.inputs["Normal"]); nt.links.new(base.outputs["Shader"], out.inputs["Surface"])
|
||||||
|
for k, v in cfg.items():
|
||||||
|
if isinstance(v, (int, float, str, bool)):
|
||||||
|
mat["CardRenderConfig." + k] = v
|
||||||
|
mat["CardRenderConfig.frontTexture"] = image.filepath
|
||||||
|
mat["CardRenderConfig.backTexture"] = BACK
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def create_simple_texture_material(name, image, roughness, coat, groups):
|
||||||
|
return create_front_material(name, image, dict(roughness=roughness, coat=coat, micro=0.04, seed=11), groups)
|
||||||
|
|
||||||
|
|
||||||
|
def create_edge_material(name, color, roughness, metallic=0.0):
|
||||||
|
m = bpy.data.materials.new(name); m.use_nodes = True
|
||||||
|
p = m.node_tree.nodes.get("Principled BSDF")
|
||||||
|
p.inputs["Base Color"].default_value = (*color, 1)
|
||||||
|
p.inputs["Roughness"].default_value = roughness
|
||||||
|
p.inputs["Metallic"].default_value = metallic
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def rounded_card_mesh():
|
||||||
|
w, h, t, r, seg = 0.063, 0.0882, 0.00040, 0.0028, 8
|
||||||
|
pts = []
|
||||||
|
for cx, cy, a0 in [
|
||||||
|
(w/2-r, h/2-r, 0), (-w/2+r, h/2-r, math.pi/2),
|
||||||
|
(-w/2+r, -h/2+r, math.pi), (w/2-r, -h/2+r, 3*math.pi/2)]:
|
||||||
|
for i in range(seg + 1):
|
||||||
|
a = a0 + (math.pi/2) * i / seg
|
||||||
|
pts.append((cx + r*math.cos(a), cy + r*math.sin(a)))
|
||||||
|
n = len(pts)
|
||||||
|
verts = [(x,y,-t/2) for x,y in pts] + [(x,y,t/2) for x,y in pts]
|
||||||
|
faces = [list(reversed(range(n))), list(range(n, 2*n))]
|
||||||
|
for i in range(n):
|
||||||
|
j = (i+1) % n
|
||||||
|
faces.append([i, j, n+j, n+i])
|
||||||
|
mesh = bpy.data.meshes.new("GEO_Card_Master_63x88")
|
||||||
|
mesh.from_pydata(verts, [], faces); mesh.update()
|
||||||
|
mesh.materials.append(None); mesh.materials.append(None); mesh.materials.append(None)
|
||||||
|
mesh.polygons[0].material_index = 1
|
||||||
|
mesh.polygons[1].material_index = 0
|
||||||
|
for p in mesh.polygons[2:]: p.material_index = 2
|
||||||
|
uv = mesh.uv_layers.new(name="UV_Card_Artwork")
|
||||||
|
for p in mesh.polygons:
|
||||||
|
for li in p.loop_indices:
|
||||||
|
vi = mesh.loops[li].vertex_index
|
||||||
|
x,y,z = mesh.vertices[vi].co
|
||||||
|
if p.index == 0:
|
||||||
|
uv.data[li].uv = (0.5-x/w, y/h+0.5)
|
||||||
|
elif p.index == 1:
|
||||||
|
uv.data[li].uv = (x/w+0.5, y/h+0.5)
|
||||||
|
else:
|
||||||
|
uv.data[li].uv = ((vi % n)/n, 0 if vi < n else 1)
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
def add_card(name, mesh, col, front_mat, back_mat, edge_mat, loc, rot):
|
||||||
|
obj = bpy.data.objects.new(name, mesh)
|
||||||
|
col.objects.link(obj)
|
||||||
|
obj.location = loc; obj.rotation_euler = rot
|
||||||
|
for idx, mat in enumerate((front_mat, back_mat, edge_mat)):
|
||||||
|
slot = obj.material_slots[idx]
|
||||||
|
slot.link = "OBJECT"; slot.material = mat
|
||||||
|
bev = obj.modifiers.new("MOD_EdgeHighlight_Bevel", "BEVEL")
|
||||||
|
bev.width = 0.00028; bev.segments = 2; bev.limit_method = "ANGLE"
|
||||||
|
obj["CardRenderConfig.frontMaterial"] = front_mat.name
|
||||||
|
obj["CardRenderConfig.backMaterial"] = back_mat.name
|
||||||
|
obj["Physical.Width_mm"] = 63.0; obj["Physical.Height_mm"] = 88.2; obj["Physical.Thickness_mm"] = 0.4; obj["Physical.CornerRadius_mm"] = 2.8
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def look_at(obj, target):
|
||||||
|
obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def add_area(name, loc, energy, size, color, target, col, shape="DISK", size_y=None):
|
||||||
|
d = bpy.data.lights.new(name, "AREA"); d.energy = energy; d.shape = shape; d.size = size; d.color = color
|
||||||
|
if size_y is not None: d.size_y = size_y
|
||||||
|
o = bpy.data.objects.new(name, d); col.objects.link(o); o.location = loc; look_at(o, target); return o
|
||||||
|
|
||||||
|
|
||||||
|
def add_camera(name, loc, target, col, ortho=None, lens=55):
|
||||||
|
d = bpy.data.cameras.new(name); d.lens = lens
|
||||||
|
if ortho is not None: d.type = "ORTHO"; d.ortho_scale = ortho
|
||||||
|
o = bpy.data.objects.new(name, d); col.objects.link(o); o.location = loc; look_at(o, target); return o
|
||||||
|
|
||||||
|
|
||||||
|
def make_label(text, loc, col, mat):
|
||||||
|
curve = bpy.data.curves.new("TXT_" + text.replace(" ", "_"), "FONT")
|
||||||
|
curve.body = text; curve.align_x = "CENTER"; curve.align_y = "CENTER"; curve.size = 0.006; curve.extrude = 0.00002
|
||||||
|
obj = bpy.data.objects.new("LABEL_" + text.replace(" ", "_"), curve); col.objects.link(obj); obj.location = loc; obj.data.materials.append(mat); return obj
|
||||||
|
|
||||||
|
|
||||||
|
clear_scene()
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.unit_settings.system = "METRIC"; scene.unit_settings.length_unit = "MILLIMETERS"; scene.unit_settings.scale_length = 1.0
|
||||||
|
scene.render.engine = "BLENDER_EEVEE"
|
||||||
|
scene.render.resolution_x = 1280; scene.render.resolution_y = 900; scene.render.resolution_percentage = 100
|
||||||
|
scene.render.image_settings.file_format = "PNG"
|
||||||
|
scene.render.film_transparent = False
|
||||||
|
scene.render.image_settings.color_mode = "RGBA"
|
||||||
|
scene.render.image_settings.color_depth = "8"
|
||||||
|
scene.render.fps = 24; scene.frame_start = 1; scene.frame_end = 120
|
||||||
|
scene.view_settings.look = "AgX - Medium High Contrast"
|
||||||
|
scene.view_settings.exposure = -1.35
|
||||||
|
scene.world.color = (0.008, 0.009, 0.012)
|
||||||
|
world = scene.world; world.use_nodes = True
|
||||||
|
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.008, 0.010, 0.014, 1)
|
||||||
|
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.012
|
||||||
|
|
||||||
|
cards_root = collection("Cards")
|
||||||
|
setup = collection("Scene_Setup")
|
||||||
|
lights_col = collection("Lights", setup); cams_col = collection("Cameras", setup); labels_col = collection("Labels", setup); env_col = collection("Environment", setup)
|
||||||
|
|
||||||
|
groups = {"surface": make_surface_texture_group(), "wear": make_wear_group(), "micro": make_micro_group(), "foil": make_foil_group(), "holo": make_holo_group(), "base": make_base_group()}
|
||||||
|
img_common = load_image(COMMON, "TEX_Timothy_Front")
|
||||||
|
img_legendary = load_image(LEGENDARY, "TEX_David_Front")
|
||||||
|
img_back = load_image(BACK, "TEX_Card_Back")
|
||||||
|
|
||||||
|
cfgs = {
|
||||||
|
"Timothy_Matte": (img_common, dict(substrate="paper", finish="matte", roughness=0.61, metallic=0.0, coat=0.02, coat_roughness=0.36, micro=0.08, linen=0.0, sheen=0.0, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=13)),
|
||||||
|
"Timothy_Linen": (img_common, dict(substrate="linen", finish="matte", roughness=0.77, metallic=0.0, coat=0.0, micro=0.045, linen=0.38, sheen=0.16, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=23)),
|
||||||
|
"Timothy_Gloss": (img_common, dict(substrate="plastic_laminate", finish="gloss", roughness=0.29, metallic=0.0, coat=0.48, coat_roughness=0.13, micro=0.015, linen=0.0, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=31)),
|
||||||
|
"David_Paper": (img_legendary, dict(substrate="premium_paper", finish="satin", roughness=0.47, metallic=0.0, coat=0.14, coat_roughness=0.24, micro=0.055, linen=0.0, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=41)),
|
||||||
|
"David_Foil": (img_legendary, dict(substrate="foil_laminate", finish="selective_foil", roughness=0.24, metallic=0.04, coat=0.44, coat_roughness=0.09, anisotropic=0.55, micro=0.018, linen=0.0, foil=0.90, foil_roughness=0.075, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=53)),
|
||||||
|
"David_Holo": (img_legendary, dict(substrate="premium_cardstock", finish="layered_holographic", roughness=0.17, metallic=0.02, coat=0.80, coat_roughness=0.055, anisotropic=0.68, micro=0.012, linen=0.0, holo_groove=0.035, foil=0.22, foil_roughness=0.085, holo=1.20, holo_scale=3.7, holo_saturation=0.82, holo_angle=1.48, thin_film_ior=1.42, wear=0.0, edge_wear=0.0, scratches=0.0, seed=67)),
|
||||||
|
"David_MetalHolo": (img_legendary, dict(substrate="metal", finish="layered_holographic", roughness=0.13, metallic=0.66, coat=0.78, coat_roughness=0.045, anisotropic=0.80, micro=0.008, linen=0.0, holo_groove=0.06, foil=0.78, foil_roughness=0.055, holo=1.45, holo_scale=4.8, holo_saturation=1.05, holo_angle=1.40, thin_film_ior=1.48, wear=0.0, edge_wear=0.0, scratches=0.0, seed=79)),
|
||||||
|
"Wear_Test": (img_common, dict(substrate="paper", finish="matte_worn", roughness=0.67, metallic=0.0, coat=0.01, coat_roughness=0.4, micro=0.10, linen=0.0, foil=0.0, holo=0.0, wear=0.11, edge_wear=0.60, scratches=0.14, seed=137)),
|
||||||
|
}
|
||||||
|
materials = {k: create_front_material("MAT_" + k, img, cfg, groups) for k, (img, cfg) in cfgs.items()}
|
||||||
|
back_mat = create_simple_texture_material("MAT_Card_Back_Universal", img_back, 0.44, 0.12, groups)
|
||||||
|
edge_paper = create_edge_material("MAT_Edge_PaperCore", (0.55, 0.48, 0.36), 0.72)
|
||||||
|
edge_plastic = create_edge_material("MAT_Edge_Plastic", (0.20, 0.22, 0.23), 0.30)
|
||||||
|
edge_metal = create_edge_material("MAT_Edge_Metal", (0.32, 0.24, 0.10), 0.18, 0.78)
|
||||||
|
|
||||||
|
mesh = rounded_card_mesh()
|
||||||
|
layout = {
|
||||||
|
"Timothy_Matte": ((-0.082, 0.061, 0), (math.radians(-2), math.radians(6), math.radians(-1.5))),
|
||||||
|
"Timothy_Linen": ((0.0, 0.061, 0), (math.radians(2), math.radians(-4), 0)),
|
||||||
|
"Timothy_Gloss": ((0.082, 0.061, 0), (math.radians(-3), math.radians(8), math.radians(1.5))),
|
||||||
|
"David_Paper": ((-0.123, -0.061, 0), (math.radians(2), math.radians(-6), math.radians(-1.5))),
|
||||||
|
"David_Foil": ((-0.041, -0.061, 0), (math.radians(-2), math.radians(8), math.radians(0.5))),
|
||||||
|
"David_Holo": ((0.041, -0.061, 0), (math.radians(3), math.radians(-11), math.radians(-0.5))),
|
||||||
|
"David_MetalHolo": ((0.123, -0.061, 0), (math.radians(-4), math.radians(13), math.radians(1.5))),
|
||||||
|
"Wear_Test": ((0.0, -0.185, 0), (0, 0, 0)),
|
||||||
|
}
|
||||||
|
objects = {}
|
||||||
|
for key, (loc, rot) in layout.items():
|
||||||
|
col = collection(key, cards_root)
|
||||||
|
edge = edge_metal if key == "David_MetalHolo" else edge_plastic if key == "Timothy_Gloss" else edge_paper
|
||||||
|
objects[key] = add_card("CARD_" + key, mesh, col, materials[key], back_mat, edge, loc, rot)
|
||||||
|
objects["Wear_Test"].hide_render = True
|
||||||
|
holo_demo_col = collection("Holo_Demo", cards_root)
|
||||||
|
holo_demo = add_card("CARD_Holo_Demo", mesh, holo_demo_col, materials["David_Holo"], back_mat, edge_paper, (0,0,0), (0,0,0))
|
||||||
|
holo_demo.hide_render = True
|
||||||
|
|
||||||
|
label_mat = bpy.data.materials.new("MAT_Label_White"); label_mat.use_nodes = True
|
||||||
|
lp = label_mat.node_tree.nodes.get("Principled BSDF"); lp.inputs["Base Color"].default_value = (0.72,0.75,0.80,1); lp.inputs["Roughness"].default_value = 0.65; lp.inputs["Emission Color"].default_value = (0.035,0.04,0.05,1); lp.inputs["Emission Strength"].default_value = 0.25
|
||||||
|
for key in ["Timothy_Matte", "Timothy_Linen", "Timothy_Gloss", "David_Paper", "David_Foil", "David_Holo", "David_MetalHolo"]:
|
||||||
|
x,y,_ = layout[key][0]
|
||||||
|
make_label(key.replace("_", " "), (x, y-0.051, 0.0015), labels_col, label_mat)
|
||||||
|
|
||||||
|
bg_mat = bpy.data.materials.new("MAT_Studio_Backdrop"); bg_mat.use_nodes = True
|
||||||
|
bp = bg_mat.node_tree.nodes.get("Principled BSDF"); bp.inputs["Base Color"].default_value = (0.012,0.015,0.022,1); bp.inputs["Roughness"].default_value = 0.82
|
||||||
|
bpy.ops.mesh.primitive_plane_add(size=2.0, location=(0,0,-0.012))
|
||||||
|
backdrop = bpy.context.object; backdrop.name = "Studio_Backdrop"; link_only(backdrop, env_col); backdrop.data.materials.append(bg_mat)
|
||||||
|
|
||||||
|
add_area("LIGHT_Key_Softbox", (-0.20,0.19,0.27), 3.0, 0.16, (1.0,0.92,0.82), (0,0,0), lights_col, "DISK")
|
||||||
|
add_area("LIGHT_Fill_Soft", (0.18,-0.14,0.22), 1.0, 0.20, (0.80,0.88,1.0), (0,0,0), lights_col, "DISK")
|
||||||
|
add_area("LIGHT_Specular_Strip", (0.24,0.10,0.16), 3.0, 0.018, (1.0,0.98,0.94), (0.025,-0.02,0), lights_col, "RECTANGLE", 0.23)
|
||||||
|
add_area("LIGHT_Top_Rim", (0.0,0.31,0.12), 1.4, 0.035, (0.90,0.94,1.0), (0,0.03,0), lights_col, "RECTANGLE", 0.20)
|
||||||
|
|
||||||
|
cam_comp = add_camera("CAM_Comparison", (0,0,0.48), (0,0,0), cams_col, ortho=0.335)
|
||||||
|
cam_front = add_camera("CAM_Front_Inspection", (0,0,0.28), (0,0,0), cams_col, ortho=0.108)
|
||||||
|
cam_three = add_camera("CAM_ThreeQuarter", (0.14,-0.040,0.28), (0,0,0), cams_col, lens=66)
|
||||||
|
cam_holo_angle = add_camera("CAM_Holo_Angle", (0.105,-0.025,0.27), (0,0,0), cams_col, lens=72)
|
||||||
|
scene.camera = cam_comp
|
||||||
|
|
||||||
|
# Holographic demo keys live on a dedicated linked instance, leaving the lineup
|
||||||
|
# card deterministic for head-on material comparisons.
|
||||||
|
holo_obj = holo_demo
|
||||||
|
holo_obj.rotation_euler = (math.radians(-6), math.radians(-18), 0); holo_obj.keyframe_insert("rotation_euler", frame=1)
|
||||||
|
holo_obj.rotation_euler = (math.radians(5), math.radians(12), math.radians(1.5)); holo_obj.keyframe_insert("rotation_euler", frame=120)
|
||||||
|
# Blender 5 uses layered Actions; the default Bezier interpolation is intentionally
|
||||||
|
# retained so the inspection motion eases naturally at each end.
|
||||||
|
scene.frame_set(1)
|
||||||
|
|
||||||
|
scene["README_RuntimeTranslation"] = "Bake/export: base color, roughness, metallic, micro-normal, static wear/imperfection masks. Runtime shader: view-dependent foil, holographic diffraction, angle-based spectrum, dynamic reflection."
|
||||||
|
scene["README_PrototypeIntent"] = "Rarity remains artwork/framing; finish remains independent physical light response. Shared mesh GEO_Card_Master_63x88 and shared NG_* node groups support parameterized variants."
|
||||||
|
scene["CardRenderConfig_Schema"] = "frontTexture, backTexture, substrate, finish, roughness, metallic, foil, holo, wear, seed"
|
||||||
|
|
||||||
|
import runpy
|
||||||
|
runpy.run_path(os.path.join(ROOT, 'blender_prototype', 'premium_finishes.py'))['apply_premium_finishes']()
|
||||||
|
runpy.run_path(os.path.join(ROOT, 'blender_prototype', 'timothy_linen_foil.py'))['add_timothy_linen_foil']()
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=BLEND)
|
||||||
|
result = {"status": "built", "blend": BLEND, "objects": list(objects.keys()), "materials": list(materials.keys()), "node_groups": list(groups.keys()), "output_dir": OUT}
|
||||||
156
blender_prototype/card_finish_material.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
"""Artwork-independent foil/holo materials. No portrait or layout coordinates.
|
||||||
|
|
||||||
|
Call create_card_finish_material with a bpy Image and an optional grayscale mask.
|
||||||
|
Mask values: 0 = printed ink only, 1 = maximum requested finish coverage.
|
||||||
|
The default automatic mask is a color heuristic, not semantic segmentation.
|
||||||
|
"""
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def socket(g, name, kind, direction='INPUT', default=None):
|
||||||
|
s=g.interface.new_socket(name=name,in_out=direction,socket_type=kind)
|
||||||
|
if default is not None:s.default_value=default
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def node(t, kind, name):
|
||||||
|
n=t.nodes.new(kind);n.name=n.label=name
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def op(t, operation, a, b=0, name=None):
|
||||||
|
n=node(t,'ShaderNodeMath',name or operation);n.operation=operation
|
||||||
|
for i,v in enumerate((a,b)):
|
||||||
|
if isinstance(v,(int,float)):n.inputs[i].default_value=v
|
||||||
|
else:t.links.new(v,n.inputs[i])
|
||||||
|
return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def smooth(t,value,lo,hi,name):
|
||||||
|
n=node(t,'ShaderNodeMapRange',name);n.clamp=True;n.interpolation_type='SMOOTHSTEP'
|
||||||
|
n.inputs['From Min'].default_value=lo;n.inputs['From Max'].default_value=hi
|
||||||
|
t.links.new(value,n.inputs['Value']);return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def auto_mask_group():
|
||||||
|
name='NG_CardFinish_AutomaticMask'
|
||||||
|
if name in bpy.data.node_groups:return bpy.data.node_groups[name]
|
||||||
|
g=bpy.data.node_groups.new(name,'ShaderNodeTree')
|
||||||
|
socket(g,'Artwork','NodeSocketColor')
|
||||||
|
socket(g,'Mask','NodeSocketFloat','OUTPUT')
|
||||||
|
i=node(g,'NodeGroupInput','Artwork');o=node(g,'NodeGroupOutput','Color based coverage')
|
||||||
|
hsv=node(g,'ShaderNodeSeparateColor','Artwork saturation and value');hsv.mode='HSV'
|
||||||
|
g.links.new(i.outputs[0],hsv.inputs[0])
|
||||||
|
saturation=smooth(g,hsv.outputs[1],.08,.58,'Favor saturated color')
|
||||||
|
value=smooth(g,hsv.outputs[2],.015,.16,'Reduce finish over dark ink')
|
||||||
|
g.links.new(op(g,'MULTIPLY',saturation,value),o.inputs[0])
|
||||||
|
for index,n in enumerate(g.nodes):n.location=(index*200,0)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def finish_group():
|
||||||
|
name='NG_CardFinish_ReflectiveCoating'
|
||||||
|
if name in bpy.data.node_groups:return bpy.data.node_groups[name]
|
||||||
|
g=bpy.data.node_groups.new(name,'ShaderNodeTree')
|
||||||
|
for nm,kind,default in [('Artwork','NodeSocketColor',None),('UV','NodeSocketVector',None),
|
||||||
|
('Normal','NodeSocketVector',None),('Coverage','NodeSocketFloat',1.0),
|
||||||
|
('Finish Strength','NodeSocketFloat',.56),('Finish Roughness','NodeSocketFloat',.19),
|
||||||
|
('Ink Roughness','NodeSocketFloat',.36),('Anisotropy','NodeSocketFloat',.32),
|
||||||
|
('Holographic','NodeSocketFloat',0.0),('Sheen','NodeSocketFloat',0.0)]:
|
||||||
|
socket(g,nm,kind,default=default)
|
||||||
|
socket(g,'Shader','NodeSocketShader','OUTPUT')
|
||||||
|
i=node(g,'NodeGroupInput','CardRenderConfig');o=node(g,'NodeGroupOutput','Card surface')
|
||||||
|
ink=node(g,'ShaderNodeBsdfPrincipled','Printed ink')
|
||||||
|
g.links.new(i.outputs['Artwork'],ink.inputs['Base Color']);g.links.new(i.outputs['Normal'],ink.inputs['Normal'])
|
||||||
|
g.links.new(i.outputs['Ink Roughness'],ink.inputs['Roughness']);g.links.new(i.outputs['Sheen'],ink.inputs['Sheen Weight'])
|
||||||
|
ink.inputs['Specular IOR Level'].default_value=.16
|
||||||
|
coat=node(g,'ShaderNodeBsdfPrincipled','Reflective coating')
|
||||||
|
coat.inputs['Metallic'].default_value=1
|
||||||
|
g.links.new(i.outputs['Finish Roughness'],coat.inputs['Roughness']);g.links.new(i.outputs['Normal'],coat.inputs['Normal'])
|
||||||
|
g.links.new(i.outputs['Anisotropy'],coat.inputs['Anisotropic'])
|
||||||
|
# Tangent from UV gradients is provided by Blender; the mesh uses its active UV.
|
||||||
|
tangent=node(g,'ShaderNodeTangent','Surface direction');tangent.direction_type='UV_MAP'
|
||||||
|
g.links.new(tangent.outputs[0],coat.inputs['Tangent'])
|
||||||
|
geo=node(g,'ShaderNodeNewGeometry','Incoming view')
|
||||||
|
transform=node(g,'ShaderNodeVectorTransform','View in card space')
|
||||||
|
transform.vector_type='VECTOR';transform.convert_from='WORLD';transform.convert_to='OBJECT'
|
||||||
|
g.links.new(geo.outputs['Incoming'],transform.inputs[0])
|
||||||
|
v=node(g,'ShaderNodeSeparateXYZ','Signed view');g.links.new(transform.outputs[0],v.inputs[0])
|
||||||
|
uv=node(g,'ShaderNodeSeparateXYZ','Spatial phase');g.links.new(i.outputs['UV'],uv.inputs[0])
|
||||||
|
phase=op(g,'ADD',op(g,'MULTIPLY',v.outputs[0],2.8),op(g,'MULTIPLY',v.outputs[1],1.6))
|
||||||
|
phase=op(g,'ADD',phase,op(g,'MULTIPLY',uv.outputs[0],.65))
|
||||||
|
phase=op(g,'ADD',phase,op(g,'MULTIPLY',uv.outputs[1],.25))
|
||||||
|
phase=op(g,'FRACT',op(g,'ADD',phase,.3))
|
||||||
|
spectrum=node(g,'ShaderNodeValToRGB','Broad reflection spectrum')
|
||||||
|
colors=[(0,(1,.32,.12,1)),(.2,(1,.84,.24,1)),(.4,(.12,1,.57,1)),(.6,(.12,.6,1,1)),(.8,(.68,.24,1,1)),(1,(1,.32,.12,1))]
|
||||||
|
spectrum.color_ramp.elements[0].color=colors[0][1];spectrum.color_ramp.elements[-1].color=colors[-1][1]
|
||||||
|
for p,c in colors[1:-1]:spectrum.color_ramp.elements.new(p).color=c
|
||||||
|
g.links.new(phase,spectrum.inputs[0])
|
||||||
|
color=node(g,'ShaderNodeMixRGB','Foil or spectral reflection')
|
||||||
|
g.links.new(i.outputs['Holographic'],color.inputs[0]);g.links.new(i.outputs['Artwork'],color.inputs[1]);g.links.new(spectrum.outputs[0],color.inputs[2])
|
||||||
|
g.links.new(color.outputs[0],coat.inputs['Base Color'])
|
||||||
|
facing=node(g,'ShaderNodeLayerWeight','Tilt activation')
|
||||||
|
angle=op(g,'ADD',.1,op(g,'MULTIPLY',smooth(g,facing.outputs['Facing'],.015,.38,'Holo tilt response'),.9))
|
||||||
|
# Lerp between constant foil coverage and view-gated holo coverage.
|
||||||
|
gate=op(g,'ADD',1,op(g,'MULTIPLY',i.outputs['Holographic'],op(g,'SUBTRACT',angle,1)))
|
||||||
|
coverage=op(g,'MULTIPLY',i.outputs['Coverage'],i.outputs['Finish Strength'])
|
||||||
|
coverage=op(g,'MINIMUM',1,op(g,'MAXIMUM',0,op(g,'MULTIPLY',coverage,gate)))
|
||||||
|
mix=node(g,'ShaderNodeMixShader','Ink plus finish')
|
||||||
|
g.links.new(coverage,mix.inputs[0]);g.links.new(ink.outputs[0],mix.inputs[1]);g.links.new(coat.outputs[0],mix.inputs[2]);g.links.new(mix.outputs[0],o.inputs[0])
|
||||||
|
for index,n in enumerate(g.nodes):n.location=((index%7)*220,-(index//7)*220)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def create_card_finish_material(name, artwork, *, finish='foil', surface_texture='smooth',
|
||||||
|
finish_mask=None, mask_mode='automatic', strength=None, roughness=None, linen_strength=.38):
|
||||||
|
"""Create/update a material; no scene, object, camera, or light mutations.
|
||||||
|
|
||||||
|
artwork / finish_mask: bpy.types.Image. Optional masks must be Non-Color data.
|
||||||
|
finish: foil | holographic. surface_texture: smooth | paper | linen.
|
||||||
|
mask_mode: automatic | full. An explicit mask takes precedence.
|
||||||
|
Updates a named material in place: its existing users see the new settings.
|
||||||
|
"""
|
||||||
|
if finish not in {'foil','holographic'}:raise ValueError('Unknown finish')
|
||||||
|
if surface_texture not in {'smooth','paper','linen'}:raise ValueError('Unknown surface texture')
|
||||||
|
if mask_mode not in {'automatic','full'}:raise ValueError('Unknown mask mode')
|
||||||
|
if not isinstance(artwork,bpy.types.Image):raise TypeError('artwork must be a Blender image')
|
||||||
|
if finish_mask is not None and finish_mask.colorspace_settings.name!='Non-Color':
|
||||||
|
raise ValueError('Load finish_mask as Non-Color data')
|
||||||
|
if surface_texture!='smooth' and 'NG_CardMicroNormal' not in bpy.data.node_groups:
|
||||||
|
raise RuntimeError('Load/build NG_CardMicroNormal before using paper or linen')
|
||||||
|
strength=(.60 if finish=='holographic' else .56) if strength is None else strength
|
||||||
|
roughness=(.23 if finish=='holographic' else .19) if roughness is None else roughness
|
||||||
|
if not 0<=strength<=1 or not 0<=roughness<=1 or not 0<=linen_strength<=1:
|
||||||
|
raise ValueError('strength, roughness, linen_strength must be between zero and one')
|
||||||
|
mat=bpy.data.materials.get(name) or bpy.data.materials.new(name)
|
||||||
|
mat.use_nodes=True;t=mat.node_tree;t.nodes.clear()
|
||||||
|
uv=node(t,'ShaderNodeTexCoord','Artwork coordinates')
|
||||||
|
tex=node(t,'ShaderNodeTexImage','Front artwork');tex.image=artwork
|
||||||
|
t.links.new(uv.outputs['UV'],tex.inputs[0])
|
||||||
|
shader=node(t,'ShaderNodeGroup','Finish parameters');shader.node_tree=finish_group()
|
||||||
|
t.links.new(tex.outputs['Color'],shader.inputs['Artwork']);t.links.new(uv.outputs['UV'],shader.inputs['UV'])
|
||||||
|
shader.inputs['Finish Strength'].default_value=strength;shader.inputs['Finish Roughness'].default_value=roughness
|
||||||
|
shader.inputs['Holographic'].default_value=float(finish=='holographic')
|
||||||
|
shader.inputs['Anisotropy'].default_value=.45 if finish=='holographic' else .32
|
||||||
|
shader.inputs['Ink Roughness'].default_value=.77 if surface_texture=='linen' else (.61 if surface_texture=='paper' else .36)
|
||||||
|
shader.inputs['Sheen'].default_value=.16 if surface_texture=='linen' else 0
|
||||||
|
if finish_mask:
|
||||||
|
mask=node(t,'ShaderNodeTexImage','Optional finish mask');mask.image=finish_mask
|
||||||
|
t.links.new(uv.outputs['UV'],mask.inputs[0]);t.links.new(mask.outputs['Color'],shader.inputs['Coverage'])
|
||||||
|
elif mask_mode=='automatic':
|
||||||
|
mask=node(t,'ShaderNodeGroup','Automatic color mask');mask.node_tree=auto_mask_group()
|
||||||
|
t.links.new(tex.outputs['Color'],mask.inputs[0]);t.links.new(mask.outputs[0],shader.inputs['Coverage'])
|
||||||
|
if surface_texture!='smooth':
|
||||||
|
micro=node(t,'ShaderNodeGroup','Surface texture');micro.node_tree=bpy.data.node_groups['NG_CardMicroNormal']
|
||||||
|
micro.inputs['Linen Strength'].default_value=linen_strength if surface_texture=='linen' else 0
|
||||||
|
micro.inputs['Micro Texture Strength'].default_value=.045 if surface_texture=='linen' else .08
|
||||||
|
t.links.new(uv.outputs['Generated'],micro.inputs['Vector']);t.links.new(micro.outputs['Normal'],shader.inputs['Normal'])
|
||||||
|
else:
|
||||||
|
geo=node(t,'ShaderNodeNewGeometry','Smooth surface normal');t.links.new(geo.outputs['Normal'],shader.inputs['Normal'])
|
||||||
|
out=node(t,'ShaderNodeOutputMaterial','Card output');t.links.new(shader.outputs[0],out.inputs[0])
|
||||||
|
for index,n in enumerate(t.nodes):n.location=((index%4)*260,-(index//4)*300)
|
||||||
|
for k,v in dict(frontTexture=artwork.filepath,finish=finish,surfaceTexture=surface_texture,
|
||||||
|
maskMode='explicit' if finish_mask else mask_mode,finishStrength=strength,
|
||||||
|
finishRoughness=roughness,linenStrength=linen_strength if surface_texture=='linen' else 0).items():
|
||||||
|
mat['CardRenderConfig.'+k]=v
|
||||||
|
return mat
|
||||||
145
blender_prototype/premium_finishes.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"""Non-destructive material-only revision; call apply_premium_finishes() in Blender.
|
||||||
|
|
||||||
|
UV protection regions are specific to legendary.png. Replace these regions when
|
||||||
|
using a different portrait; they are an editable prototype mask, not segmentation.
|
||||||
|
"""
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def node(tree, kind, name):
|
||||||
|
n = tree.nodes.new(kind)
|
||||||
|
n.name = n.label = name
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def math_node(t, op, a, b=0, name=None):
|
||||||
|
n = node(t, 'ShaderNodeMath', name or op)
|
||||||
|
n.operation = op
|
||||||
|
for i, value in enumerate((a, b)):
|
||||||
|
if isinstance(value, (float, int)):
|
||||||
|
n.inputs[i].default_value = value
|
||||||
|
else:
|
||||||
|
t.links.new(value, n.inputs[i])
|
||||||
|
return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def ramp(t, value, lo, hi, name):
|
||||||
|
n = node(t, 'ShaderNodeMapRange', name)
|
||||||
|
n.clamp = True
|
||||||
|
n.interpolation_type = 'SMOOTHSTEP'
|
||||||
|
n.inputs['From Min'].default_value = lo
|
||||||
|
n.inputs['From Max'].default_value = hi
|
||||||
|
t.links.new(value, n.inputs['Value'])
|
||||||
|
return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def ellipse(t, uv, cx, cy, rx, ry, name):
|
||||||
|
x = math_node(t, 'DIVIDE', math_node(t, 'SUBTRACT', uv.outputs[0], cx), rx)
|
||||||
|
y = math_node(t, 'DIVIDE', math_node(t, 'SUBTRACT', uv.outputs[1], cy), ry)
|
||||||
|
d = math_node(t, 'ADD', math_node(t, 'MULTIPLY', x, x), math_node(t, 'MULTIPLY', y, y))
|
||||||
|
return ramp(t, d, .65, 1.2, name)
|
||||||
|
|
||||||
|
|
||||||
|
def make_mask():
|
||||||
|
name = 'NG_David_PrintProtection'
|
||||||
|
if name in bpy.data.node_groups:
|
||||||
|
return bpy.data.node_groups[name]
|
||||||
|
g = bpy.data.node_groups.new(name, 'ShaderNodeTree')
|
||||||
|
for nm, st in [('Artwork', 'NodeSocketColor'), ('UV', 'NodeSocketVector')]:
|
||||||
|
g.interface.new_socket(name=nm, in_out='INPUT', socket_type=st)
|
||||||
|
g.interface.new_socket(name='Finish Mask', in_out='OUTPUT', socket_type='NodeSocketFloat')
|
||||||
|
inp = node(g, 'NodeGroupInput', 'Source artwork and UV')
|
||||||
|
out = node(g, 'NodeGroupOutput', 'Protected decorative finish mask')
|
||||||
|
uv = node(g, 'ShaderNodeSeparateXYZ', 'Artwork coordinates')
|
||||||
|
g.links.new(inp.outputs['UV'], uv.inputs[0])
|
||||||
|
hsv = node(g, 'ShaderNodeSeparateColor', 'Saturation selection')
|
||||||
|
hsv.mode = 'HSV'
|
||||||
|
g.links.new(inp.outputs['Artwork'], hsv.inputs[0])
|
||||||
|
mask = ramp(g, hsv.outputs[1], .18, .62, 'Saturated glass and gold')
|
||||||
|
mask = math_node(g, 'MULTIPLY', mask, ramp(g, hsv.outputs[2], .015, .16, 'Protect dark ink'))
|
||||||
|
for cx, cy, rx, ry, label in [(.50,.72,.19,.23,'Face and neck'),
|
||||||
|
(.64,.44,.17,.13,'Hand'),
|
||||||
|
(.29,.44,.18,.18,'Lamb')]:
|
||||||
|
mask = math_node(g, 'MULTIPLY', mask, ellipse(g, uv,cx,cy,rx,ry,label))
|
||||||
|
# Soft central rectangle covering cream title/rules panel; ornament remains.
|
||||||
|
x = math_node(g,'ABSOLUTE',math_node(g,'SUBTRACT',uv.outputs[0],.5))
|
||||||
|
side = ramp(g,x,.34,.41,'Keep side ornament')
|
||||||
|
above = ramp(g,uv.outputs[1],.245,.29,'Protect title panel')
|
||||||
|
below = math_node(g,'SUBTRACT',1,ramp(g,uv.outputs[1],.07,.105,'Keep bottom ornament'))
|
||||||
|
panel = math_node(g,'MAXIMUM',side,math_node(g,'MAXIMUM',above,below))
|
||||||
|
mask = math_node(g,'MULTIPLY',mask,panel)
|
||||||
|
g.links.new(mask,out.inputs[0])
|
||||||
|
for i,n in enumerate(g.nodes): n.location=((i%8)*185,-(i//8)*170)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def apply_premium_finishes():
|
||||||
|
mask_group = make_mask()
|
||||||
|
variants = [('MAT_David_Foil',False,False),('MAT_David_Holo',True,False),
|
||||||
|
('MAT_David_MetalHolo',True,True)]
|
||||||
|
for name,holo,metal in variants:
|
||||||
|
mat=bpy.data.materials[name]
|
||||||
|
t=mat.node_tree
|
||||||
|
image=next(n.image for n in t.nodes if n.type=='TEX_IMAGE')
|
||||||
|
t.nodes.clear()
|
||||||
|
uv=node(t,'ShaderNodeTexCoord','Card UV')
|
||||||
|
tex=node(t,'ShaderNodeTexImage','Printed David artwork');tex.image=image
|
||||||
|
t.links.new(uv.outputs['UV'],tex.inputs['Vector'])
|
||||||
|
mask=node(t,'ShaderNodeGroup','Protected finish regions');mask.node_tree=mask_group
|
||||||
|
t.links.new(tex.outputs['Color'],mask.inputs['Artwork']);t.links.new(uv.outputs['UV'],mask.inputs['UV'])
|
||||||
|
ink=node(t,'ShaderNodeBsdfPrincipled','Printed ink layer')
|
||||||
|
t.links.new(tex.outputs['Color'],ink.inputs['Base Color'])
|
||||||
|
ink.inputs['Roughness'].default_value=.36
|
||||||
|
ink.inputs['Coat Weight'].default_value=0
|
||||||
|
ink.inputs['Specular IOR Level'].default_value=.16
|
||||||
|
ink.inputs['Coat Roughness'].default_value=.24
|
||||||
|
finish=node(t,'ShaderNodeBsdfPrincipled','Diffractive reflection' if holo else 'Metal foil underprint')
|
||||||
|
finish.inputs['Metallic'].default_value=1
|
||||||
|
finish.inputs['Roughness'].default_value=.23 if holo else .19
|
||||||
|
finish.inputs['Anisotropic'].default_value=.45 if holo else .32
|
||||||
|
tangent=node(t,'ShaderNodeTangent','UV groove direction');tangent.direction_type='UV_MAP';tangent.uv_map='UV_Card_Artwork'
|
||||||
|
t.links.new(tangent.outputs[0],finish.inputs['Tangent'])
|
||||||
|
strength=node(t,'ShaderNodeValue','Finish Strength');strength.outputs[0].default_value=.72 if metal else (.60 if holo else .56)
|
||||||
|
weight=math_node(t,'MULTIPLY',mask.outputs[0],strength.outputs[0],'Masked coating coverage')
|
||||||
|
if holo:
|
||||||
|
geo=node(t,'ShaderNodeNewGeometry','View direction')
|
||||||
|
transform=node(t,'ShaderNodeVectorTransform','View in card coordinates')
|
||||||
|
transform.vector_type='VECTOR';transform.convert_from='WORLD';transform.convert_to='OBJECT'
|
||||||
|
t.links.new(geo.outputs['Incoming'],transform.inputs['Vector'])
|
||||||
|
v=node(t,'ShaderNodeSeparateXYZ','Signed view components');t.links.new(transform.outputs[0],v.inputs[0])
|
||||||
|
p=node(t,'ShaderNodeSeparateXYZ','Broad spatial phase');t.links.new(uv.outputs['UV'],p.inputs[0])
|
||||||
|
phase=math_node(t,'ADD',math_node(t,'MULTIPLY',v.outputs[0],2.8),math_node(t,'MULTIPLY',v.outputs[1],1.6))
|
||||||
|
phase=math_node(t,'ADD',phase,math_node(t,'MULTIPLY',p.outputs[0],.65))
|
||||||
|
phase=math_node(t,'ADD',phase,math_node(t,'MULTIPLY',p.outputs[1],.25))
|
||||||
|
phase=math_node(t,'FRACT',math_node(t,'ADD',phase,.3))
|
||||||
|
spectrum=node(t,'ShaderNodeValToRGB','Broad diffraction spectrum')
|
||||||
|
colors=[(0,(1,.32,.12,1)),(.2,(1,.84,.24,1)),(.4,(.12,1,.57,1)),(.6,(.12,.6,1,1)),(.8,(.68,.24,1,1)),(1,(1,.32,.12,1))]
|
||||||
|
for e in list(spectrum.color_ramp.elements)[1:-1]:spectrum.color_ramp.elements.remove(e)
|
||||||
|
spectrum.color_ramp.elements[0].color=colors[0][1];spectrum.color_ramp.elements[-1].color=colors[-1][1]
|
||||||
|
for pos,c in colors[1:-1]:spectrum.color_ramp.elements.new(pos).color=c
|
||||||
|
t.links.new(phase,spectrum.inputs[0]);t.links.new(spectrum.outputs[0],finish.inputs['Base Color'])
|
||||||
|
facing=node(t,'ShaderNodeLayerWeight','Grazing angle activation')
|
||||||
|
activation=ramp(t,facing.outputs['Facing'],.015,.38,'Tilt response')
|
||||||
|
activation=math_node(t,'ADD',.10,math_node(t,'MULTIPLY',activation,.90))
|
||||||
|
weight=math_node(t,'MULTIPLY',weight,activation,'Angle gated coating')
|
||||||
|
else:
|
||||||
|
t.links.new(tex.outputs['Color'],finish.inputs['Base Color'])
|
||||||
|
mix=node(t,'ShaderNodeMixShader','Printed ink plus reflective finish')
|
||||||
|
t.links.new(weight,mix.inputs[0]);t.links.new(ink.outputs[0],mix.inputs[1]);t.links.new(finish.outputs[0],mix.inputs[2])
|
||||||
|
out=node(t,'ShaderNodeOutputMaterial','Card surface');t.links.new(mix.outputs[0],out.inputs[0])
|
||||||
|
for i,n in enumerate(t.nodes):n.location=((i%7)*220,-(i//7)*240)
|
||||||
|
mat['Finish revision']='Reflective coating v3; protected print; no diffuse rainbow or thin-film outlines'
|
||||||
|
mat['CardRenderConfig.finishStrength']=strength.outputs[0].default_value
|
||||||
|
mat['CardRenderConfig.roughness']=finish.inputs['Roughness'].default_value
|
||||||
|
mat['CardRenderConfig.holo']=1.0 if holo else 0.0
|
||||||
|
mat['CardRenderConfig.holo_groove']=0.0
|
||||||
|
mat['CardRenderConfig.metallic']=1.0
|
||||||
|
# Low-power vertical strip intersects the existing demo's +12 degree tilt.
|
||||||
|
from mathutils import Vector
|
||||||
|
strip=bpy.data.objects['LIGHT_Specular_Strip']
|
||||||
|
strip.location=(.10,.012,.23)
|
||||||
|
strip.rotation_euler=(Vector((0,0,0))-strip.location).to_track_quat('-Z','Y').to_euler()
|
||||||
|
strip.data.energy=.65
|
||||||
|
strip.data.size=.018
|
||||||
|
strip.data.size_y=.14
|
||||||
|
return {'materials':[v[0] for v in variants],'mask':mask_group.name}
|
||||||
45
blender_prototype/render_linen_foil_example.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""Review at matched poses. Does not alter saved scene state."""
|
||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
s=bpy.context.scene
|
||||||
|
out=Path('/home/dkzver/dev/sanctification-tcg/blender_prototype/renders')
|
||||||
|
objs=[o for o in s.objects if o.name.startswith(('CARD_','LABEL_'))]
|
||||||
|
saved={o.name:(o.location.copy(),o.rotation_euler.copy(),o.hide_render) for o in objs}
|
||||||
|
cam=bpy.data.objects['CAM_Front_Inspection']
|
||||||
|
cs=(cam.location.copy(),cam.rotation_euler.copy(),cam.data.ortho_scale)
|
||||||
|
settings=(s.camera,s.render.resolution_x,s.render.resolution_y,s.render.filepath)
|
||||||
|
def isolate(name,angle):
|
||||||
|
for o in objs:o.hide_render=o.name!=name
|
||||||
|
card=bpy.data.objects[name];card.location=(0,0,0);card.rotation_euler=(0,math.radians(angle),0)
|
||||||
|
s.camera=cam;s.render.resolution_x=750;s.render.resolution_y=1050
|
||||||
|
return card
|
||||||
|
def shot(filename):
|
||||||
|
s.render.filepath=str(out/filename);bpy.ops.render.render(write_still=True)
|
||||||
|
try:
|
||||||
|
isolate('CARD_Timothy_LinenFoil',0);shot('timothy_linen_foil_head_on.png')
|
||||||
|
isolate('CARD_Timothy_LinenFoil',12);shot('timothy_linen_foil_reflection.png')
|
||||||
|
for name in ['Timothy_Linen','Timothy_LinenFoil']:
|
||||||
|
isolate('CARD_'+name,12);cam.data.ortho_scale=.036;cam.location.y=-.018
|
||||||
|
shot(name.lower()+'_macro.png');cam.location=cs[0];cam.data.ortho_scale=cs[2]
|
||||||
|
for o in objs:o.hide_render=True
|
||||||
|
for name,x,label_text in [('Timothy_Linen',-.038,'Linen / Matte'),('Timothy_LinenFoil',.038,'Linen / Foil')]:
|
||||||
|
card=bpy.data.objects['CARD_'+name];card.hide_render=False;card.location=(x,0,0);card.rotation_euler=(0,math.radians(12),0)
|
||||||
|
label=bpy.data.objects['LABEL_'+name];label.hide_render=False;label.location=(x,-.050,.0015)
|
||||||
|
s.camera=cam;cam.data.ortho_scale=.16;s.render.resolution_x=1400;s.render.resolution_y=1050
|
||||||
|
shot('timothy_linen_vs_linen_foil.png')
|
||||||
|
if globals().get('RENDER_MOTION',False):
|
||||||
|
card=isolate('CARD_Timothy_LinenFoil',0)
|
||||||
|
s.render.resolution_x=540;s.render.resolution_y=720;cam.data.ortho_scale=.108
|
||||||
|
frames=out/'linen_foil_frames';frames.mkdir(exist_ok=True)
|
||||||
|
for f in range(72):
|
||||||
|
angle=-18+36*(.5-.5*math.cos(math.pi*f/71))
|
||||||
|
card.rotation_euler=(0,math.radians(angle),0)
|
||||||
|
shot('linen_foil_frames/linen_foil_%04d.png'%(f+1))
|
||||||
|
finally:
|
||||||
|
for o in objs:
|
||||||
|
loc,rot,h=saved[o.name];o.location=loc;o.rotation_euler=rot;o.hide_render=h
|
||||||
|
cam.location,cam.rotation_euler,cam.data.ortho_scale=cs
|
||||||
|
s.camera,s.render.resolution_x,s.render.resolution_y,s.render.filepath=settings
|
||||||
|
result={'directory':str(out),'restored_scene':True}
|
||||||
57
blender_prototype/render_premium_review.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""Run with Blender MCP. REVIEW_ANIMATION=True also regenerates demo frames."""
|
||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
scene=bpy.context.scene
|
||||||
|
out=Path('/home/dkzver/dev/sanctification-tcg/blender_prototype/renders')
|
||||||
|
cards=[o for o in scene.objects if o.name.startswith('CARD_')]
|
||||||
|
labels=[o for o in scene.objects if o.name.startswith('LABEL_')]
|
||||||
|
saved={o.name:(o.location.copy(),o.rotation_euler.copy(),o.hide_render) for o in cards+labels}
|
||||||
|
camera=scene.camera
|
||||||
|
resolution=(scene.render.resolution_x,scene.render.resolution_y)
|
||||||
|
frame=scene.frame_current
|
||||||
|
path=scene.render.filepath
|
||||||
|
def render(name,target=None,tilt=0):
|
||||||
|
for o in cards:o.hide_render=(o.name!=target if target else o.name in {'CARD_Holo_Demo','CARD_Wear_Test'})
|
||||||
|
for o in labels:o.hide_render=target is not None
|
||||||
|
scene.camera=bpy.data.objects['CAM_Front_Inspection' if target else 'CAM_Comparison']
|
||||||
|
scene.render.resolution_x=750 if target else 1280
|
||||||
|
scene.render.resolution_y=1050 if target else 900
|
||||||
|
if target:
|
||||||
|
o=bpy.data.objects[target];o.location=(0,0,0);o.rotation_euler=(0,math.radians(tilt),0)
|
||||||
|
scene.render.image_settings.file_format='PNG'
|
||||||
|
scene.render.filepath=str(out/name)
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
if target:
|
||||||
|
o.location=saved[target][0];o.rotation_euler=saved[target][1]
|
||||||
|
try:
|
||||||
|
render('comparison_lineup.png')
|
||||||
|
render('timothy_matte_closeup.png','CARD_Timothy_Matte')
|
||||||
|
render('timothy_linen_closeup.png','CARD_Timothy_Linen',-14)
|
||||||
|
render('timothy_gloss_closeup.png','CARD_Timothy_Gloss',8)
|
||||||
|
render('david_paper_closeup.png','CARD_David_Paper')
|
||||||
|
render('wear_test_timothy.png','CARD_Wear_Test')
|
||||||
|
render('universal_card_back.png','CARD_Timothy_Matte',180)
|
||||||
|
render('david_foil_closeup.png','CARD_David_Foil',0)
|
||||||
|
render('david_foil_reflection.png','CARD_David_Foil',12)
|
||||||
|
render('david_holographic_head_on.png','CARD_David_Holo',0)
|
||||||
|
render('david_holographic_spectrum_angle.png','CARD_David_Holo',12)
|
||||||
|
render('david_holographic_opposite_angle.png','CARD_David_Holo',-18)
|
||||||
|
render('card_three_quarter_thickness.png','CARD_David_Foil',55)
|
||||||
|
if globals().get('REVIEW_ANIMATION',False):
|
||||||
|
for o in cards:o.hide_render=o.name!='CARD_Holo_Demo'
|
||||||
|
for o in labels:o.hide_render=True
|
||||||
|
scene.camera=bpy.data.objects['CAM_Front_Inspection']
|
||||||
|
scene.render.resolution_x=540;scene.render.resolution_y=720
|
||||||
|
(out/'holo_frames').mkdir(exist_ok=True)
|
||||||
|
scene.render.filepath=str(out/'holo_frames'/'holo_')
|
||||||
|
bpy.ops.render.render(animation=True)
|
||||||
|
finally:
|
||||||
|
scene.frame_set(frame)
|
||||||
|
for o in cards+labels:
|
||||||
|
loc,rot,hidden=saved[o.name];o.location=loc;o.rotation_euler=rot;o.hide_render=hidden
|
||||||
|
scene.camera=camera
|
||||||
|
scene.render.resolution_x,scene.render.resolution_y=resolution
|
||||||
|
scene.render.filepath=path
|
||||||
|
result={'render_directory':str(out),'animation':globals().get('REVIEW_ANIMATION',False)}
|
||||||
BIN
blender_prototype/renders/card_three_quarter_thickness.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
blender_prototype/renders/comparison_lineup.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
blender_prototype/renders/comparison_material_revision.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
blender_prototype/renders/darker_lighting_preview.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
blender_prototype/renders/david_foil_closeup.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
blender_prototype/renders/david_foil_reflection.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
blender_prototype/renders/david_holographic_head_on.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
blender_prototype/renders/david_holographic_opposite_angle.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
blender_prototype/renders/david_holographic_spectrum_angle.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
blender_prototype/renders/david_paper_closeup.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
blender_prototype/renders/foil_material_revision.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0001.png
Normal file
|
After Width: | Height: | Size: 1011 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0002.png
Normal file
|
After Width: | Height: | Size: 1010 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0003.png
Normal file
|
After Width: | Height: | Size: 1010 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0004.png
Normal file
|
After Width: | Height: | Size: 1011 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0005.png
Normal file
|
After Width: | Height: | Size: 1011 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0006.png
Normal file
|
After Width: | Height: | Size: 1011 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0007.png
Normal file
|
After Width: | Height: | Size: 1012 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0008.png
Normal file
|
After Width: | Height: | Size: 1012 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0009.png
Normal file
|
After Width: | Height: | Size: 1013 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0010.png
Normal file
|
After Width: | Height: | Size: 1013 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0011.png
Normal file
|
After Width: | Height: | Size: 1015 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0012.png
Normal file
|
After Width: | Height: | Size: 1016 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0013.png
Normal file
|
After Width: | Height: | Size: 1017 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0014.png
Normal file
|
After Width: | Height: | Size: 1018 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0015.png
Normal file
|
After Width: | Height: | Size: 1018 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0016.png
Normal file
|
After Width: | Height: | Size: 1020 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0017.png
Normal file
|
After Width: | Height: | Size: 1021 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0018.png
Normal file
|
After Width: | Height: | Size: 1022 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0019.png
Normal file
|
After Width: | Height: | Size: 1024 KiB |
BIN
blender_prototype/renders/holo_frames/holo_0020.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0021.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0022.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0023.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0024.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0025.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0026.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0027.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0028.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0029.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0030.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0031.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0032.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0033.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0034.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0035.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0036.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0037.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0038.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0039.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0040.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0041.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0042.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0043.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0044.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0045.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0046.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0047.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0048.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0049.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0050.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0051.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0052.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0053.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0054.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0055.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0056.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0057.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0058.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0059.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0060.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0061.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0062.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0063.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0064.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0065.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0066.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0067.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0068.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0069.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0070.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0071.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0072.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0073.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0074.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0075.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0076.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0077.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0078.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0079.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0080.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0081.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
blender_prototype/renders/holo_frames/holo_0082.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |