Oh fix other half
@@ -1,845 +0,0 @@
|
|||||||
# Sanctification TCG - Art Direction
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
I want the cards and the product as a whole to have a very distinct visual identity.
|
|
||||||
|
|
||||||
The current direction I like most is:
|
|
||||||
|
|
||||||
> **Stained-glass-inspired as the main style, with tapestry and illuminated manuscript influence as supporting texture and ornament.**
|
|
||||||
|
|
||||||
The overall goal should be:
|
|
||||||
|
|
||||||
> **Sacred, beautiful, and stylized. Not a dense museum reconstruction.**
|
|
||||||
|
|
||||||
I do not want the cards to feel like a generic fantasy TCG, casino game, anime card game, or hyper-realistic historical reenactment.
|
|
||||||
|
|
||||||
The art should feel reverent and Christian first, while still being visually strong enough that collecting the cards feels exciting.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Core Visual Principles
|
|
||||||
|
|
||||||
Across the whole product, the artwork should generally follow these ideas:
|
|
||||||
|
|
||||||
- Sacred and reverent tone.
|
|
||||||
- Stained-glass-inspired composition.
|
|
||||||
- Jewel-tone palette where appropriate.
|
|
||||||
- Gold can be used as an accent, but should become more prominent as rarity increases.
|
|
||||||
- Tapestry, linen, parchment, and illuminated-manuscript textures can support the overall look.
|
|
||||||
- Clear subject silhouettes.
|
|
||||||
- Cards should remain readable at thumbnail size.
|
|
||||||
- Avoid hyper-photorealism.
|
|
||||||
- Avoid anime or comic-book energy.
|
|
||||||
- Avoid a dark fantasy aesthetic.
|
|
||||||
- Avoid visual language that feels like gambling, loot boxes, or a casino.
|
|
||||||
- Avoid overly dense historical reconstruction.
|
|
||||||
- Prefer symbolic clarity over excessive detail.
|
|
||||||
- Every card should feel like it belongs to the same product, even when different sets or traditions have their own visual flavor.
|
|
||||||
|
|
||||||
A good way to think about the stained-glass direction is that the cards should feel inspired by sacred windows and devotional art without needing to literally reproduce any specific historical style.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Product-Wide Visual Language
|
|
||||||
|
|
||||||
The card art should influence the rest of the app.
|
|
||||||
|
|
||||||
Possible shared UI motifs:
|
|
||||||
|
|
||||||
- Cathedral-window or arched shapes.
|
|
||||||
- Restrained ornamental dividers.
|
|
||||||
- Deep blue, burgundy, cream, green, and gold accents.
|
|
||||||
- Parchment, stone, linen, or subtle glass-inspired surfaces.
|
|
||||||
- Illuminated-manuscript-style decorative elements used sparingly.
|
|
||||||
- Soft lighting rather than neon effects.
|
|
||||||
- Ornament should support hierarchy rather than fill every empty space.
|
|
||||||
|
|
||||||
The application UI should still feel modern and usable.
|
|
||||||
|
|
||||||
The goal is not to make the entire interface look medieval.
|
|
||||||
|
|
||||||
The historical / sacred art direction should mostly come through:
|
|
||||||
|
|
||||||
- Cards.
|
|
||||||
- Pack openings.
|
|
||||||
- Binder / collection presentation.
|
|
||||||
- Headers and section treatments.
|
|
||||||
- Decorative accents.
|
|
||||||
- Important moments and milestones.
|
|
||||||
|
|
||||||
Normal application controls should remain clear and familiar.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Card Art
|
|
||||||
|
|
||||||
## General Composition
|
|
||||||
|
|
||||||
Cards should generally prioritize:
|
|
||||||
|
|
||||||
1. Subject.
|
|
||||||
2. Identity.
|
|
||||||
3. Short supporting content.
|
|
||||||
4. Ornamentation.
|
|
||||||
|
|
||||||
The art should be readable even when the card is relatively small.
|
|
||||||
|
|
||||||
The card should not require the user to zoom in just to understand who or what it represents.
|
|
||||||
|
|
||||||
For character cards, prefer:
|
|
||||||
|
|
||||||
- One clear subject.
|
|
||||||
- Strong silhouette.
|
|
||||||
- Limited background complexity.
|
|
||||||
- Symbolic items associated with the subject.
|
|
||||||
- Calm, iconic poses over excessive action scenes unless the card itself represents an event.
|
|
||||||
|
|
||||||
For event cards, larger compositions and multiple figures can be used when necessary.
|
|
||||||
|
|
||||||
For locations, items, doctrines, councils, and other categories, the framing can change while still staying within the same overall stained-glass visual language.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Text on the Card
|
|
||||||
|
|
||||||
The physical card should stay concise.
|
|
||||||
|
|
||||||
The app itself can provide the deeper educational content when the card is inspected.
|
|
||||||
|
|
||||||
A normal card can contain:
|
|
||||||
|
|
||||||
- Name.
|
|
||||||
- Short subtitle or descriptor.
|
|
||||||
- Short verse, quote, or historical line.
|
|
||||||
- Reference.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
> **David**
|
|
||||||
> Shepherd King
|
|
||||||
>
|
|
||||||
> “The LORD is my shepherd...”
|
|
||||||
> Psalm 23:1
|
|
||||||
|
|
||||||
The front content priority should generally be:
|
|
||||||
|
|
||||||
1. Scripture directly related to the card.
|
|
||||||
2. Historically significant Christian quotation.
|
|
||||||
3. Concise historical or theological statement.
|
|
||||||
|
|
||||||
We should not force a full verse onto the card when it harms the visual design.
|
|
||||||
|
|
||||||
A short excerpt with an ellipsis is fine when it is clearly intentional.
|
|
||||||
|
|
||||||
The full content can appear in the inspection panel.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Card Back
|
|
||||||
|
|
||||||
The card back should generally be standardized so it does not reveal the identity of a card during pack opening.
|
|
||||||
|
|
||||||
The back should not leak:
|
|
||||||
|
|
||||||
- Card identity.
|
|
||||||
- Exact rarity.
|
|
||||||
- Printing.
|
|
||||||
- Finish.
|
|
||||||
- Material.
|
|
||||||
- Other traits that would spoil the reveal.
|
|
||||||
|
|
||||||
It can use the Sanctification branding and common visual language.
|
|
||||||
|
|
||||||
Different sets could potentially have different backs in the future, but only if this does not meaningfully spoil the card reveal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Card Inspection
|
|
||||||
|
|
||||||
The card itself should remain relatively clean.
|
|
||||||
|
|
||||||
More detailed information belongs in the inspection experience.
|
|
||||||
|
|
||||||
When inspecting a card, the user should be able to:
|
|
||||||
|
|
||||||
- Rotate the card freely.
|
|
||||||
- Flip between front and back.
|
|
||||||
- Zoom in to inspect artwork and physical characteristics.
|
|
||||||
- Observe material properties responding dynamically to lighting.
|
|
||||||
- Observe wear and imperfections where applicable.
|
|
||||||
- Return quickly to the originating collection / binder context.
|
|
||||||
|
|
||||||
The inspection UI should also expose the educational and collectible information that does not need to live directly on the physical card.
|
|
||||||
|
|
||||||
Possible sections:
|
|
||||||
|
|
||||||
## About
|
|
||||||
|
|
||||||
- Full verse or quote.
|
|
||||||
- Description.
|
|
||||||
- Historical context.
|
|
||||||
- Theological context.
|
|
||||||
- Related Scripture.
|
|
||||||
- Related events / people / cards.
|
|
||||||
- Learn-more sources.
|
|
||||||
|
|
||||||
## Card
|
|
||||||
|
|
||||||
- Rarity.
|
|
||||||
- Set.
|
|
||||||
- Printing.
|
|
||||||
- Finish.
|
|
||||||
- Material.
|
|
||||||
- Wear classification.
|
|
||||||
- Exact wear percentage.
|
|
||||||
|
|
||||||
## Provenance
|
|
||||||
|
|
||||||
- Original owner.
|
|
||||||
- Date opened.
|
|
||||||
- Population.
|
|
||||||
- Serial / instance information.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 should have a major impact on the visual grammar of the card.
|
|
||||||
|
|
||||||
A common card should not look almost as impressive as a legendary card.
|
|
||||||
|
|
||||||
Rarity should primarily affect:
|
|
||||||
|
|
||||||
- Frame complexity.
|
|
||||||
- Number of ornamental elements.
|
|
||||||
- Visual storytelling.
|
|
||||||
- Palette richness.
|
|
||||||
- Composition complexity.
|
|
||||||
- Symbolic detail.
|
|
||||||
- Amount of gold.
|
|
||||||
- Background complexity.
|
|
||||||
|
|
||||||
Rarity should **not** simply mean adding more glow or visual effects.
|
|
||||||
|
|
||||||
A useful principle:
|
|
||||||
|
|
||||||
> **Higher rarity should add storytelling and ornamentation, not merely more effects.**
|
|
||||||
|
|
||||||
Another way to think about it:
|
|
||||||
|
|
||||||
> **Common cards should feel like a simple parish stained-glass window. Legendary cards should feel closer to the centerpiece window behind the altar.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common
|
|
||||||
|
|
||||||
Common cards should be intentionally restrained.
|
|
||||||
|
|
||||||
### Border
|
|
||||||
|
|
||||||
- Thin and simple.
|
|
||||||
- Geometric.
|
|
||||||
- Very little decorative ornament.
|
|
||||||
- No elaborate corner medallions.
|
|
||||||
- No complex illuminated-manuscript scrollwork.
|
|
||||||
- No gold.
|
|
||||||
|
|
||||||
### Color
|
|
||||||
|
|
||||||
- Limited palette.
|
|
||||||
- Roughly 2-3 dominant colors plus cream / parchment.
|
|
||||||
- More muted or restrained jewel tones.
|
|
||||||
- Gold used mostly for separation or small accents.
|
|
||||||
|
|
||||||
### Artwork
|
|
||||||
|
|
||||||
- It doesn't need to be just the subject but rather them in the background doing something
|
|
||||||
- Simple background.
|
|
||||||
- Larger stained-glass panes.
|
|
||||||
- Fewer small glass segments.
|
|
||||||
- Minimal symbolic elements.
|
|
||||||
- Calm composition.
|
|
||||||
- No unnecessary visual drama.
|
|
||||||
- Minimal micro-detail.
|
|
||||||
|
|
||||||
### Overall Feel
|
|
||||||
|
|
||||||
- Humble.
|
|
||||||
- Clean.
|
|
||||||
- Grounded.
|
|
||||||
- Accessible.
|
|
||||||
- Still beautiful, but obviously not premium.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
**Timothy**
|
|
||||||
|
|
||||||
- Timothy holding a scroll.
|
|
||||||
- Simple ancient street or landscape.
|
|
||||||
- Simple arch.
|
|
||||||
- Blue / green / cream palette.
|
|
||||||
- Thin brass or gold divider.
|
|
||||||
- No ornate botanical frame.
|
|
||||||
- No special corner symbols.
|
|
||||||
- No heavily gilded title plaque.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Uncommon
|
|
||||||
|
|
||||||
Uncommon cards can begin introducing more refinement.
|
|
||||||
|
|
||||||
### Border
|
|
||||||
|
|
||||||
- Slightly thicker frame.
|
|
||||||
- Small decorative accents.
|
|
||||||
- Limited corner ornament.
|
|
||||||
- Some leaf or manuscript-inspired motifs.
|
|
||||||
|
|
||||||
### Color
|
|
||||||
|
|
||||||
- Slightly broader palette.
|
|
||||||
- More saturated jewel tones.
|
|
||||||
- Small use of gold.
|
|
||||||
|
|
||||||
### Artwork
|
|
||||||
|
|
||||||
- More detailed backgrounds.
|
|
||||||
- Additional symbolic element.
|
|
||||||
- Slightly more complex stained-glass segmentation.
|
|
||||||
- More deliberate framing around the subject.
|
|
||||||
|
|
||||||
### Overall Feel
|
|
||||||
|
|
||||||
- Refined.
|
|
||||||
- Noticeably more interesting than Common.
|
|
||||||
- Still not highly ornate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## Rare
|
|
||||||
|
|
||||||
Rare cards should feel distinctive.
|
|
||||||
|
|
||||||
### Border
|
|
||||||
|
|
||||||
- Decorative frame.
|
|
||||||
- More custom ornament.
|
|
||||||
- Symbolic motifs related to the subject.
|
|
||||||
|
|
||||||
### Color
|
|
||||||
|
|
||||||
- Richer jewel-tone palette.
|
|
||||||
- More contrast.
|
|
||||||
- More use of gold.
|
|
||||||
|
|
||||||
### Artwork
|
|
||||||
|
|
||||||
- Stronger visual storytelling.
|
|
||||||
- More complex stained-glass structures.
|
|
||||||
- Multiple meaningful background elements.
|
|
||||||
- More deliberate lighting composition.
|
|
||||||
|
|
||||||
### Overall Feel
|
|
||||||
|
|
||||||
- Distinctive.
|
|
||||||
- Collectible.
|
|
||||||
- Clearly above the normal card experience.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Extraordinary
|
|
||||||
|
|
||||||
Extraordinary cards should feel prestigious.
|
|
||||||
|
|
||||||
### Border
|
|
||||||
|
|
||||||
- Elaborate framing.
|
|
||||||
- Strong illuminated-manuscript influence.
|
|
||||||
- More bespoke ornamentation.
|
|
||||||
- Larger use of gold.
|
|
||||||
|
|
||||||
### Color
|
|
||||||
|
|
||||||
- Deep and saturated jewel tones.
|
|
||||||
- Broad palette while remaining cohesive.
|
|
||||||
- Strong visual contrast.
|
|
||||||
|
|
||||||
### Artwork
|
|
||||||
|
|
||||||
- Complex composition.
|
|
||||||
- Multiple symbolic elements.
|
|
||||||
- Richer environments.
|
|
||||||
- More detailed stained-glass segmentation.
|
|
||||||
- Greater sense of scale or drama.
|
|
||||||
|
|
||||||
### Overall Feel
|
|
||||||
|
|
||||||
- Prestigious.
|
|
||||||
- Elaborate.
|
|
||||||
- Immediately special.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Legendary
|
|
||||||
|
|
||||||
Legendary cards should be visually unmistakable.
|
|
||||||
|
|
||||||
They can break some of the normal card-layout rules while still clearly belonging to the same product.
|
|
||||||
|
|
||||||
### Border
|
|
||||||
|
|
||||||
- Highly bespoke.
|
|
||||||
- Custom symbolic elements related specifically to the card.
|
|
||||||
- Extensive but tasteful ornamentation.
|
|
||||||
- Rich gilding.
|
|
||||||
|
|
||||||
### Color
|
|
||||||
|
|
||||||
- Full jewel-tone palette where appropriate.
|
|
||||||
- Strong gold accents.
|
|
||||||
- Deep color contrast.
|
|
||||||
|
|
||||||
### Artwork
|
|
||||||
|
|
||||||
- Strong visual storytelling.
|
|
||||||
- Multiple symbols connected directly to the subject.
|
|
||||||
- Complex stained-glass composition.
|
|
||||||
- More dramatic light.
|
|
||||||
- More detailed environments.
|
|
||||||
- Unique framing or iconography.
|
|
||||||
|
|
||||||
### Overall Feel
|
|
||||||
|
|
||||||
- Immediately special.
|
|
||||||
- Reverent.
|
|
||||||
- Iconic.
|
|
||||||
- Worth stopping to inspect.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
**David**
|
|
||||||
|
|
||||||
Possible visual elements:
|
|
||||||
|
|
||||||
- Crown.
|
|
||||||
- Harp.
|
|
||||||
- Shepherd staff.
|
|
||||||
- Sheep.
|
|
||||||
- Jerusalem.
|
|
||||||
- Star / celestial imagery.
|
|
||||||
- Bespoke gold framing.
|
|
||||||
- Rich red and blue garments.
|
|
||||||
- Complex stained-glass composition.
|
|
||||||
|
|
||||||
These elements help tell David's story rather than simply adding decoration for decoration's sake.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
#### Alt Style - Tapestry
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Rarity vs. Physical Treatment
|
|
||||||
|
|
||||||
Rarity, material, and finish are separate systems.
|
|
||||||
|
|
||||||
This is important.
|
|
||||||
|
|
||||||
A Common holographic card should still visually read as a Common card.
|
|
||||||
|
|
||||||
A Legendary plain-paper card should still visually read as Legendary.
|
|
||||||
|
|
||||||
## Rarity controls
|
|
||||||
|
|
||||||
- Graphic design.
|
|
||||||
- Frame.
|
|
||||||
- Composition.
|
|
||||||
- Symbolic storytelling.
|
|
||||||
- Base art complexity.
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- A treatment applied over the material and printed artwork.
|
|
||||||
- Selective foil reflection.
|
|
||||||
- Holographic spectral response.
|
|
||||||
- Coating strength and coverage.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
- Common Paper + Printed Ink Timothy.
|
|
||||||
- Common Paper + Foil Timothy.
|
|
||||||
- Common Linen + Holographic Timothy.
|
|
||||||
- Common Metal + Printed Ink Timothy.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
If a collectible property exists, the player should ideally be able to perceive it visually or interactively.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- Finish -> visible.
|
|
||||||
- Material -> visible.
|
|
||||||
- Wear -> visible.
|
|
||||||
- Imperfections -> visible.
|
|
||||||
- Printing -> visible.
|
|
||||||
- Weight -> indirectly observable.
|
|
||||||
- Provenance -> 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
|
|
||||||
|
|
||||||
- Matte with fine, irregular fibers.
|
|
||||||
- Broad, weak reflection rather than a sharp sheen.
|
|
||||||
- Grain should become visible during close inspection without making the artwork noisy.
|
|
||||||
|
|
||||||
## Plastic
|
|
||||||
|
|
||||||
- Smoother surface.
|
|
||||||
- Clear-coated dielectric reflection.
|
|
||||||
- A bounded moving highlight should remain visible even with Printed Ink.
|
|
||||||
- Cleaner edges.
|
|
||||||
|
|
||||||
## Linen
|
|
||||||
|
|
||||||
- Raised interlaced warp and weft rather than generic noise.
|
|
||||||
- Soft diffuse reflection.
|
|
||||||
- Small shaded recesses should help the weave read under moving light.
|
|
||||||
- Particularly compatible with tapestry-inspired artwork.
|
|
||||||
|
|
||||||
## Metal
|
|
||||||
|
|
||||||
- Reflective underprint and a visibly metallic edge.
|
|
||||||
- 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.
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- Selective angle-dependent metallic reflection controlled by an art-directed mask.
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- View-dependent spectral response.
|
|
||||||
- Holographic response should use masks so intentional portions of the artwork react.
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Wear
|
|
||||||
|
|
||||||
Wear should not make cards ugly to the point that the artwork becomes undesirable.
|
|
||||||
|
|
||||||
Even the lowest wear levels should remain visually readable and collectible.
|
|
||||||
|
|
||||||
A 0.00 wear card can be noticeably scratched, worn, or damaged while still looking like a cared-for physical collectible rather than a destroyed object.
|
|
||||||
|
|
||||||
Possible classifications are still subject to change, but could look something like:
|
|
||||||
|
|
||||||
- Mint.
|
|
||||||
- Near Mint.
|
|
||||||
- Minimal Wear.
|
|
||||||
- Moderately Played.
|
|
||||||
- Worn.
|
|
||||||
|
|
||||||
Wear can affect:
|
|
||||||
|
|
||||||
- Edge whitening.
|
|
||||||
- Surface scratches.
|
|
||||||
- Small scuffs.
|
|
||||||
- Minor print abrasion.
|
|
||||||
- Subtle corner wear.
|
|
||||||
|
|
||||||
It should not normally obscure:
|
|
||||||
|
|
||||||
- Subject identity.
|
|
||||||
- Important text.
|
|
||||||
- Major portions of the artwork.
|
|
||||||
|
|
||||||
Wear patterns should ideally be seeded per card instance so two cards with the same wear value do not necessarily have identical damage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Printing Variants
|
|
||||||
|
|
||||||
Printing should affect the graphic presentation of the same card identity.
|
|
||||||
|
|
||||||
## Normal
|
|
||||||
|
|
||||||
- Standard frame.
|
|
||||||
- Name.
|
|
||||||
- Subtitle.
|
|
||||||
- Short verse / quote / fact.
|
|
||||||
- Normal artwork area.
|
|
||||||
|
|
||||||
## Borderless
|
|
||||||
|
|
||||||
- Artwork extends farther toward the card edges.
|
|
||||||
- Reduced frame.
|
|
||||||
- Minimal text treatment.
|
|
||||||
|
|
||||||
## Textless
|
|
||||||
|
|
||||||
- Artwork-focused.
|
|
||||||
- Little or no text.
|
|
||||||
- Educational information remains available in the inspection panel.
|
|
||||||
|
|
||||||
## Boundless
|
|
||||||
|
|
||||||
- Full-bleed or near-full-bleed artwork.
|
|
||||||
- Minimal or absent conventional frame.
|
|
||||||
- Intended to feel like a special printing rather than a higher rarity.
|
|
||||||
|
|
||||||
Printing type should not change the underlying rarity of the card.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Thumbnail Readability
|
|
||||||
|
|
||||||
All normal card designs should remain recognizable in a binder or collection grid.
|
|
||||||
|
|
||||||
At thumbnail size, the user should ideally still be able to identify:
|
|
||||||
|
|
||||||
- The subject or general composition.
|
|
||||||
- The card rarity through its frame language.
|
|
||||||
- Major color identity.
|
|
||||||
- Whether the card has an unusual printing or finish when practical.
|
|
||||||
|
|
||||||
Fine text does not need to remain readable at thumbnail size.
|
|
||||||
|
|
||||||
The card name should become readable as the card grows larger or when selected.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Pack Opening Art Direction
|
|
||||||
|
|
||||||
Pack opening should create anticipation without feeling like a casino.
|
|
||||||
|
|
||||||
Rarity can affect reveal pacing and presentation.
|
|
||||||
|
|
||||||
Possible progression:
|
|
||||||
|
|
||||||
## Common
|
|
||||||
|
|
||||||
- Simple lift.
|
|
||||||
- Quick flip.
|
|
||||||
- Minimal lighting change.
|
|
||||||
|
|
||||||
## Uncommon
|
|
||||||
|
|
||||||
- Slight pause.
|
|
||||||
- Mild environmental emphasis.
|
|
||||||
|
|
||||||
## Rare
|
|
||||||
|
|
||||||
- More deliberate reveal.
|
|
||||||
- Stronger lighting transition.
|
|
||||||
- Small audio / visual flourish.
|
|
||||||
|
|
||||||
## Extraordinary
|
|
||||||
|
|
||||||
- More dramatic reveal timing.
|
|
||||||
- Larger environmental shift.
|
|
||||||
- More pronounced presentation.
|
|
||||||
|
|
||||||
## Legendary
|
|
||||||
|
|
||||||
- Unique or near-unique reveal treatment.
|
|
||||||
- Strong visual emphasis.
|
|
||||||
- Longer anticipation, but still skippable.
|
|
||||||
|
|
||||||
The goal is:
|
|
||||||
|
|
||||||
> **Rarity can affect anticipation without going full casino.**
|
|
||||||
|
|
||||||
All reveal animations should remain skippable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Things to Avoid
|
|
||||||
|
|
||||||
Across all rarities:
|
|
||||||
|
|
||||||
- Hyper-photorealistic character portraits.
|
|
||||||
- Anime styling.
|
|
||||||
- Comic-book styling.
|
|
||||||
- Neon fantasy effects.
|
|
||||||
- Generic fantasy RPG framing.
|
|
||||||
- Excessive glowing particles.
|
|
||||||
- Overly busy ornament.
|
|
||||||
- Illegible typography.
|
|
||||||
- Decorative detail that makes the subject harder to identify.
|
|
||||||
- Making every card look premium.
|
|
||||||
- Using rarity only as a glow / particle intensity slider.
|
|
||||||
- Turning historical Christian art into parody.
|
|
||||||
- Making Common cards deliberately ugly.
|
|
||||||
|
|
||||||
The distinction should be:
|
|
||||||
|
|
||||||
> **Common is simple. Legendary is elaborate.**
|
|
||||||
|
|
||||||
Not:
|
|
||||||
|
|
||||||
> **Common is bad. Legendary is good.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# MVP Art Goals
|
|
||||||
|
|
||||||
For the MVP, I do not need every card to be a full-scale art project.
|
|
||||||
|
|
||||||
The interesting part is proving that the entire visual system works.
|
|
||||||
|
|
||||||
The MVP should establish:
|
|
||||||
|
|
||||||
- A consistent stained-glass-inspired card style.
|
|
||||||
- A clearly readable rarity system.
|
|
||||||
- A reusable card layout system.
|
|
||||||
- At least one working example of each rarity.
|
|
||||||
- Multiple materials.
|
|
||||||
- Multiple finishes.
|
|
||||||
- Multiple printing types.
|
|
||||||
- Wear.
|
|
||||||
- 3D card inspection.
|
|
||||||
- Light-reactive materials.
|
|
||||||
- Pack-opening presentation.
|
|
||||||
- Thumbnail / binder representations.
|
|
||||||
|
|
||||||
I think it makes more sense to create a smaller number of highly intentional art examples and then build the other cards from the same visual system rather than independently designing all 50 cards.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Open Art Questions
|
|
||||||
|
|
||||||
Things that still need to be explored:
|
|
||||||
|
|
||||||
- Exact card dimensions / aspect ratio.
|
|
||||||
- Final typography.
|
|
||||||
- Exact color palette.
|
|
||||||
- Whether each rarity has a fixed base frame or a family of frames.
|
|
||||||
- Whether card categories have their own secondary visual motifs.
|
|
||||||
- Whether tradition-specific cards alter UI or border accents.
|
|
||||||
- Exact visual treatment for controversies.
|
|
||||||
- Whether halos should be used based on historical iconography, card category, tradition, or purely artistic composition.
|
|
||||||
- How much motion should exist inside holographic cards.
|
|
||||||
- Exact distinction between Extraordinary and Legendary.
|
|
||||||
- Whether higher-rarity cards are always visually more complex or occasionally gain prestige through extreme simplicity.
|
|
||||||
- How much the card artwork itself changes across printing variants.
|
|
||||||
- Final card back design.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Current Direction Summary
|
|
||||||
|
|
||||||
The current visual north star is:
|
|
||||||
|
|
||||||
> **Stained glass first. Tapestry and illuminated manuscript second. Sacred and stylized throughout.**
|
|
||||||
|
|
||||||
Rarity should be obvious through design language before the user reads a label.
|
|
||||||
|
|
||||||
Common cards should be restrained and humble.
|
|
||||||
|
|
||||||
Legendary cards should feel bespoke and narratively rich.
|
|
||||||
|
|
||||||
Finishes and materials should enhance the physical card without replacing rarity as the primary graphic hierarchy.
|
|
||||||
|
|
||||||
The cards should be beautiful enough to want to collect, but the overall product should always remain reverent rather than flashy.
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,593 +0,0 @@
|
|||||||
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}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
"""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}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
"""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}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
"""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)}
|
|
||||||
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1011 KiB |
|
Before Width: | Height: | Size: 1010 KiB |
|
Before Width: | Height: | Size: 1010 KiB |
|
Before Width: | Height: | Size: 1011 KiB |
|
Before Width: | Height: | Size: 1011 KiB |
|
Before Width: | Height: | Size: 1011 KiB |
|
Before Width: | Height: | Size: 1012 KiB |
|
Before Width: | Height: | Size: 1012 KiB |
|
Before Width: | Height: | Size: 1013 KiB |
|
Before Width: | Height: | Size: 1013 KiB |
|
Before Width: | Height: | Size: 1015 KiB |
|
Before Width: | Height: | Size: 1016 KiB |
|
Before Width: | Height: | Size: 1017 KiB |
|
Before Width: | Height: | Size: 1018 KiB |
|
Before Width: | Height: | Size: 1018 KiB |
|
Before Width: | Height: | Size: 1020 KiB |
|
Before Width: | Height: | Size: 1021 KiB |
|
Before Width: | Height: | Size: 1022 KiB |
|
Before Width: | Height: | Size: 1024 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |