init
760
art-direction.md
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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. Finish
|
||||||
|
|
||||||
|
Rarity 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.
|
||||||
|
|
||||||
|
## Finish controls
|
||||||
|
|
||||||
|
- Physical rendering behavior.
|
||||||
|
- Reflectivity.
|
||||||
|
- Foil effects.
|
||||||
|
- Holographic behavior.
|
||||||
|
- Surface texture.
|
||||||
|
- Material response to lighting.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
- Common Paper Timothy.
|
||||||
|
- Common Foil Timothy.
|
||||||
|
- Common Holographic Timothy.
|
||||||
|
- Common Metal Timothy.
|
||||||
|
|
||||||
|
All of them should still use the same underlying Common Timothy card design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Paper
|
||||||
|
|
||||||
|
- Matte or slightly textured.
|
||||||
|
- Soft reflection.
|
||||||
|
- Visible paper grain if inspected closely.
|
||||||
|
|
||||||
|
## Plastic
|
||||||
|
|
||||||
|
- Smoother surface.
|
||||||
|
- Slightly stronger specular response.
|
||||||
|
- Cleaner edges.
|
||||||
|
|
||||||
|
## Linen
|
||||||
|
|
||||||
|
- Fine woven normal map.
|
||||||
|
- Soft diffuse reflection.
|
||||||
|
- Particularly compatible with tapestry-inspired artwork.
|
||||||
|
|
||||||
|
## Metal
|
||||||
|
|
||||||
|
- Strong metallic response.
|
||||||
|
- Sharper reflections.
|
||||||
|
- Visually heavier.
|
||||||
|
- Should still preserve artwork readability.
|
||||||
|
|
||||||
|
## Foil
|
||||||
|
|
||||||
|
- Angle-dependent reflection.
|
||||||
|
- Light sweeps visibly across the card.
|
||||||
|
|
||||||
|
## Holographic
|
||||||
|
|
||||||
|
- View-dependent spectral response.
|
||||||
|
- Holographic effect should ideally use masks so only specific portions of the artwork react.
|
||||||
|
- Should remain tasteful and avoid a neon / fantasy appearance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.
|
||||||
358
functional-spec-v1.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# Christian Habit Tracker TCG - Functional Spec
|
||||||
|
|
||||||
|
I think a good name for the application would be Sanctification TSG; as the objective is to grow in Christ and move towards holiness.
|
||||||
|
|
||||||
|
Scenario: Habit trackers are super common place and incredibly overdone. I find the same issue with every single one of them - tracking your habits needs to be a habit itself. Many applications try to add a "carrot" but nothing actually seems to provide a good sense to really reel you back in.
|
||||||
|
On the opposite front, bad habits don't really have a negative attached to them.
|
||||||
|
|
||||||
|
One thing that has become hugely popular is TCGs - more importantly card openings. This idea that you can collect the cards and try to obtain rare instances or wide variations of said cards.
|
||||||
|
|
||||||
|
All these card games are typically built in their own universe - but I want to do it christian themed.
|
||||||
|
|
||||||
|
Below breaks this down into the large functional pieces of the project - and the unknowns that may need to be explored
|
||||||
|
|
||||||
|
|
||||||
|
## Christianity
|
||||||
|
|
||||||
|
At the core foundation, we believe to be Christianity is founded in the words and logic of the Nicene Creed, as follows:
|
||||||
|
|
||||||
|
"We believe in one God the Father Almighty, Maker of heaven and earth, and of all things visible and invisible.
|
||||||
|
|
||||||
|
“And in one Lord Jesus Christ, the only-begotten Son of God, begotten of the Father before all worlds, God of God, Light of Light, Very God of Very God, begotten, not made, being of one substance with the Father by whom all things were made; who for us men, and for our salvation, came down from heaven, and was incarnate by the Holy Spirit of the Virgin Mary, and was made man, and was crucified also for us under Pontius Pilate. He suffered and was buried, and the third day he rose again according to the Scriptures, and ascended into heaven, and sitteth on the right hand of the Father. And he shall come again with glory to judge both the quick and the dead, whose kingdom shall have no end.
|
||||||
|
|
||||||
|
“And we believe in the Holy Spirit, the Lord and Giver of Life, who proceedeth from the Father, who with the Father and the Son together is worshiped and glorified, who spoke by the prophets. And we believe in one holy catholic and apostolic Church. We acknowledge one baptism for the remission of sins. And we look for the resurrection of the dead, and the life of the world to come. Amen"
|
||||||
|
|
||||||
|
## Functional Break Down
|
||||||
|
|
||||||
|
### TCG
|
||||||
|
|
||||||
|
The whole premise of the "game" is just collection. This would become the carrot of your habits and hope to build something a bit more fun and community into the service.
|
||||||
|
|
||||||
|
Daily login would bring in daily packs and each task can also give the daily packs or even special rare packs (that have slightly boosted numbers - or even guaranteed) for different tasks/streaks/achievements etc.
|
||||||
|
|
||||||
|
In terms of how we want to calculate rarity and mathematics, I want to make it so that if you want to collect every single card yourself, it should take something like at least a year of consistent with doing each task.
|
||||||
|
* if you do collect all cards (regardless of quality, etc), I think i might be cool to have some kind of prestige system. Maybe decorates profile/binder?
|
||||||
|
|
||||||
|
I was thinking that if you somehow are able to complete the full compendium (not the types) then you can do it in like 1 year. (subject to change depending on feedback)
|
||||||
|
|
||||||
|
#### Binders
|
||||||
|
|
||||||
|
Everyone gets a binder that holds their entire compendium that they can look through (or others can look at)
|
||||||
|
|
||||||
|
Later more binders and different kinds of card holders can be unlocked from achievements or small microtransations - I haven't decided
|
||||||
|
* This would allow people to collect different types (all metal, all holo, all foil, etc, etc)
|
||||||
|
|
||||||
|
#### The Packs
|
||||||
|
|
||||||
|
* Streaks
|
||||||
|
* Weekly/monthly streaks will provide higher quality packs.
|
||||||
|
|
||||||
|
* Not sure yet but I would want different kinds of packs that provide boosted chances at the better tiers/values
|
||||||
|
* Common - normal color foil
|
||||||
|
* Gold
|
||||||
|
* Illuminesent
|
||||||
|
|
||||||
|
* Different packs would have different opening animations
|
||||||
|
* You can choose to have all in a row and clicking each card to flip them
|
||||||
|
* Or you can have them stacked and flip them going from front to back.
|
||||||
|
* You can also have just auto open which just opens and start flipping all of them in an animation.
|
||||||
|
|
||||||
|
#### The Cards
|
||||||
|
|
||||||
|
* Different flavors of cards (different decks) - they would share common cards (common not as in tier but rather common theology)
|
||||||
|
* Catholic
|
||||||
|
* Orthodox
|
||||||
|
* Protestant
|
||||||
|
* Heretic
|
||||||
|
|
||||||
|
* Different Tiers (I want to find better Biblical names for tiers)
|
||||||
|
* Common
|
||||||
|
* Uncommon
|
||||||
|
* Rare
|
||||||
|
* Extraordinary
|
||||||
|
* Legendary
|
||||||
|
* Trinity - Highest tier - only The Father, The Son, and The Holy Spirit
|
||||||
|
|
||||||
|
* Quality - To make the trading aspect fun and always continuous cards pulled will also roll different qualities on a log scale (or some kind of mathematical scale we think about) - maybe even normal graph for things like the wear
|
||||||
|
* Wear
|
||||||
|
* Every card unpacked would have a float value between 1.00 and 0.00. 1 would be prestine but 0 would be very worn out.
|
||||||
|
* Legendary and Trinity (or I guess if we add any other really high tiers would be float locked to being higher)
|
||||||
|
* Finish - matte/glossy/textured/holographic/foil
|
||||||
|
* Very small chance for different kinds of holographic/foils - also float capped
|
||||||
|
* Printing
|
||||||
|
* Normal, Borderless, Textless, Boundless
|
||||||
|
* Material
|
||||||
|
* Paper, plastic, wood, linen, metal
|
||||||
|
* Weight
|
||||||
|
* All cards would be about 100g with light variance but things like holographic/foil would add a minor amount of weight
|
||||||
|
* We could have an achievement system that let's you get a small digital scale so you can weigh the pack to make for more fun unpacking.
|
||||||
|
* Imperfections
|
||||||
|
* This one is still a maybe - we can generate a couple of masks that look like finger prints, hair, etc. for the card that has a very small change to be added to imperfections
|
||||||
|
* Trinity cannot have imperfections.
|
||||||
|
|
||||||
|
* Content
|
||||||
|
* I would want to shoot to have a very large repository of cards
|
||||||
|
* The cards would have different categories: people, history, icon, item, place, event, theologions, theorems (apologetics), etc.
|
||||||
|
* The greater the impact of that card in Biblical history the higher the tier.
|
||||||
|
* So more obscure people would be put into common (even something like "man") while someone like David would be considered Legendary (could be extraordinary).
|
||||||
|
* Something like Gabriel or Michael would be also legendary
|
||||||
|
* Unless a specifically type of card (like textless), every card would have some kind of text associated with it
|
||||||
|
* The text must be something related to that card - whether a verse, quote, or historical fact (priority in that order)
|
||||||
|
* The text would also have a link when clicked on to allow you to learn more or see why.
|
||||||
|
|
||||||
|
* Cards also retain metadata about when it was open and who opened it. Maybe we could also track how many cards of that kind (card id/finish/printing/material) - with this you can tell if you have a 1 of 1 metalic holographic normal print Adam or if there were already 1000 pulled. Weight/wear/imperfections wouldn't go into this.
|
||||||
|
|
||||||
|
* Art style
|
||||||
|
* I still haven't decided how I want the art/cards to look. Just need to AI generate a few options to get a common design
|
||||||
|
* Card favors would affect the colors, borders of the cards, and back art of the cards.
|
||||||
|
|
||||||
|
### Habit tracking
|
||||||
|
|
||||||
|
Ultimately this would be habit tracker at its base.
|
||||||
|
|
||||||
|
#### Honor system
|
||||||
|
|
||||||
|
I think an important aspect to outline is that ultimately this is a tool to build essential habits with God and the community/fun aspect is second nature.
|
||||||
|
How or whether someone actually is doing what they are tracking is between them and God - and with accountability partners if they choose to do that.
|
||||||
|
|
||||||
|
What is means to complete a task is also between the user and God. If prayer for them means 5 minutes or 1 hour, that is fine. BUT I think we should outline (maybe during first time) that trying to say a 5 second prayer before entering the app is not qualification. It is a matter of the heart - not to gamify things.
|
||||||
|
|
||||||
|
#### Positive Habit Tracking
|
||||||
|
|
||||||
|
I think initially we will start with a couple of base good habits to build on:
|
||||||
|
* Prayer
|
||||||
|
* Bible reading
|
||||||
|
* Weekly church attendance - Weekly tasks would give a different kind of pack - or maybe we just give you the option to chose a type of common pack
|
||||||
|
* Study - this would be something like a devotional, a chapter of christian literature, or digging into theology
|
||||||
|
* Volunteering ?* - not sure how to track this cadence. Doesn't feel like a daily thing but also a category of things that builds a christian value
|
||||||
|
|
||||||
|
You can always log up to the day prior to recieve your card pack
|
||||||
|
* You can always log previous days/weeks of progress towards your habits but you will not recieve packs for those. (Streaks will also be reset)
|
||||||
|
|
||||||
|
#### Negative Habit Tracking
|
||||||
|
|
||||||
|
I want there to be an option to track bad habits that someone may have. These can be custom input although we will provide some base line ones like: lust, addiction (smoking, drinking), anger, etc.
|
||||||
|
|
||||||
|
This will track separately from the positive habits that the user is trying to break.
|
||||||
|
They can open up the exact issues to accountability partners or more generic message "Like, X user needs your prayer :)"
|
||||||
|
|
||||||
|
As for if you do track your negative habits, whenever you break your streaks, you will need to burn one of the cards at random. That card would be gone forever.
|
||||||
|
My thought on this is it would help provide a tangible reaction to what happens.
|
||||||
|
|
||||||
|
BUT after each broken streak, we would provide messaging that the user is still loved and Christ died for our sins but we must repent and choose to grow with Christ
|
||||||
|
|
||||||
|
### Social Interaction
|
||||||
|
|
||||||
|
The other important aspect of card collections is trading.
|
||||||
|
In general having a community that you can hook into. Where you can have friends and even accountability partners (friends don't necessarily mean accountability partners as some people might rather choose to keep anonimity)
|
||||||
|
|
||||||
|
No chat integration - this would require too much moderation and other things that can take away resources.
|
||||||
|
You can send each other pre-determined messages of encouragement, you can send any verse (I guess you can technically make it bad but I think we will trust in good faith for this), or pre-determined messages to help with
|
||||||
|
|
||||||
|
Functionalities I think would be great:
|
||||||
|
* View other people's compendium.
|
||||||
|
* Offer trade
|
||||||
|
* All habit tracking will be private except can be opened up to accountability partners.
|
||||||
|
* This avoids hubris or shame - that is not the point of the project.
|
||||||
|
|
||||||
|
#### Integrations
|
||||||
|
|
||||||
|
I was thinking to provide API and webhook support so it can be integrated into anything at that point. Something like a Fluxer bot (Discord also)
|
||||||
|
|
||||||
|
Other integrations I am not sure yet.
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
|
||||||
|
Reorganize to:
|
||||||
|
|
||||||
|
1. Product principles
|
||||||
|
2. User journeys
|
||||||
|
3. Core gameplay loop
|
||||||
|
4. Habit model
|
||||||
|
5. Reward/economy model
|
||||||
|
6. Card definition
|
||||||
|
7. Card instance/variant model
|
||||||
|
8. Collection/binder
|
||||||
|
9. Social/trading
|
||||||
|
10. Accountability/privacy
|
||||||
|
11. Christianity/content policy
|
||||||
|
12. Monetization principles
|
||||||
|
13. MVP
|
||||||
|
14. Future systems
|
||||||
|
15. Open questions
|
||||||
|
|
||||||
|
|
||||||
|
Application
|
||||||
|
└── React / Next.js
|
||||||
|
├── Normal UI
|
||||||
|
│ ├── habits
|
||||||
|
│ ├── profile
|
||||||
|
│ ├── trading
|
||||||
|
│ ├── settings
|
||||||
|
│ └── binder navigation
|
||||||
|
│
|
||||||
|
└── React Three Fiber
|
||||||
|
└── Three.js
|
||||||
|
├── card inspector
|
||||||
|
├── pack opening
|
||||||
|
├── binder card interactions
|
||||||
|
└── collection showcase
|
||||||
|
|
||||||
|
Asset pipeline
|
||||||
|
└── Blender
|
||||||
|
└── GLB / glTF
|
||||||
|
├── geometry
|
||||||
|
├── base materials
|
||||||
|
├── normal maps
|
||||||
|
└── other PBR maps
|
||||||
|
|
||||||
|
|
||||||
|
### Card Interaction
|
||||||
|
|
||||||
|
Cards should be represented as tactile physical objects rather
|
||||||
|
than static collectible images wherever practical.
|
||||||
|
|
||||||
|
When inspecting a card, the user must 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.
|
||||||
|
|
||||||
|
Card finish and material must have visually distinguishable
|
||||||
|
properties. For example, foil, holographic, paper, linen, and
|
||||||
|
metal cards should respond differently to lighting.
|
||||||
|
|
||||||
|
Lower-power devices must be able to fall back to simplified
|
||||||
|
visual representations without affecting gameplay.
|
||||||
|
|
||||||
|
## Initial Technical Direction
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
React / Next.js
|
||||||
|
|
||||||
|
3D:
|
||||||
|
Three.js via React Three Fiber
|
||||||
|
|
||||||
|
Asset authoring:
|
||||||
|
Blender
|
||||||
|
|
||||||
|
Asset format:
|
||||||
|
glTF / GLB
|
||||||
|
|
||||||
|
Rendering:
|
||||||
|
WebGL2 baseline
|
||||||
|
WebGPU optional
|
||||||
|
|
||||||
|
Card architecture:
|
||||||
|
Shared geometry + runtime textures/material parameters
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
honesty vs. the economy
|
||||||
|
* I don't think this is as big of a problem. I would build the rarity and economy as if everyone did earnestly do every single task and complete every streak
|
||||||
|
* If people miss or don't, then the economy would just not develop as quickly
|
||||||
|
* If people lie, that is fine because that is technically my baseline (as I want everyone - including myself - to be in a place where you can truthfully say you take care of it all the time)
|
||||||
|
* "Five minutes of prayer shouldn't mint less valuable cards than sixty minutes, because you'd immediately incentivize exaggerated reporting."
|
||||||
|
* Yeah. I thought that is what I outlined in the functional spec. We would give guidance in messaging but a whole day in prayer doesn't give you anything more than 5 minutes or lying. But it is habit tracking afterall.
|
||||||
|
|
||||||
|
Privacy
|
||||||
|
* I think we just need to ensure encryption on all customer data. Server cannot read customer content. We can do statistics but things like sin/anethema would be encrypted. Maybe something like BYOK to ensure server admin really can't just look in the DB
|
||||||
|
* Accountability is obscure by default.
|
||||||
|
|
||||||
|
Negative-habit punishment
|
||||||
|
* I actually fully agree with you on this.
|
||||||
|
* I like both the Abstinence streak and the honesty/engagement aspect
|
||||||
|
* I like recovery milestones
|
||||||
|
* Every fall needs to be met with grace and good messaging
|
||||||
|
* Every milestone needs to be celebrated (not with packs - we don't want to promote any kind of side thing)
|
||||||
|
* I still like the concept of burning the card but ceremoniously or voluntarily would be great!
|
||||||
|
|
||||||
|
Streaks
|
||||||
|
* You're right. I don't think we do streaks - consistency is a stronger metric.
|
||||||
|
|
||||||
|
Shared core plus traditions
|
||||||
|
* I like this. Puts strong emphasis on learning but keeps the divide
|
||||||
|
* I do want to make sure to keep things like Mormonism and JW (for example), in the controversies
|
||||||
|
|
||||||
|
Trinity
|
||||||
|
* This is just something I was initially rumiating about; I agree that it doesn't have a place in this as I wrote originally.
|
||||||
|
* What if instead - during the tutorial (or if you skip tutorial) - the Trinity would be given to you as your first and permanent card.
|
||||||
|
* This symbolizes the core aspect that Christ is always with us and is a gift we do not lose (theology here obviously differs but point stands).
|
||||||
|
* You can customize it to whichever finish etc you like. It will always be in mint condition.
|
||||||
|
* Thoughts on verse? I almost think John 3:16 - seems like a pretty decent fit.
|
||||||
|
* But having Trinitarian other concepts is nice too but I think the meaning here is good.
|
||||||
|
|
||||||
|
Rarity should probably not mean spiritual importance
|
||||||
|
* I agree on the prominence within the collection
|
||||||
|
* It was an initial idea I wanted to start with but didn't come to a full idea of what that might be
|
||||||
|
* "And perhaps some sets have independent rarity structures." <- I think this is great too.
|
||||||
|
|
||||||
|
Monitization
|
||||||
|
* I genuinely have no real plan to make money off this. I half mentioned it if this were to somehow blow up in popularity.
|
||||||
|
* My current plan is to build it out and try it with my friends
|
||||||
|
* ABSOLUTELY no loot boxes. Gambling already ruins lives and that would be an anethema to what I'm trying to accomplish
|
||||||
|
|
||||||
|
Login pack
|
||||||
|
* I still need to deliberate on how I want to give packs and how many cards per pack and statistics behind that.
|
||||||
|
* Login pack helps establish the initial hook. When in the app (or could before), you now might realize, "Oh right. I need to actually accomplish my tasks"
|
||||||
|
* I'm also still not sure if we should give pack per task or upgrade the pack per task
|
||||||
|
* Hmmm. What if that is the case? The login pack has reaally bad odds. You do your dailies and other tasks and it upgrades your pack.
|
||||||
|
* This way you still have the hook, if they choose to just open it - the odds are bad or maybe we just heavily increase the wear chance.
|
||||||
|
|
||||||
|
Random thoughts
|
||||||
|
* You can also choose to save packs to open later - if you want to open many at once or on occasion.
|
||||||
|
|
||||||
|
MVP
|
||||||
|
* I like the idea of fleshing out the MVP.
|
||||||
|
* I'm a software engineer - not an artist or 3D modeler - so I'll be leaning on LLMs (MCP servers) for help on those pieces
|
||||||
|
* There is not true deliverable so I think just making like 50 cards but trying to do all the difficult stuff with the cards is what is interesting to me
|
||||||
|
* The canvas and rendering will also be interesting tech challenges
|
||||||
|
* Wiring up a backend to client and handling all those other pieces are much easier to me and will be addressed in tech spec
|
||||||
|
|
||||||
|
|
||||||
|
Product philosophy
|
||||||
|
* Completion/honesty/consistency/return from failure are all core concepts
|
||||||
|
|
||||||
|
Is the collectible system subordinate to the habits, or equally important?
|
||||||
|
* I think the collection system is what makes the habit tracking unique - if I wanted habit tracking, I can do with pen/paper or just download any one of the 1000 apps that already do so.
|
||||||
|
|
||||||
|
Christianity
|
||||||
|
Who determines whether card content is doctrinally acceptable?
|
||||||
|
Is the standard strictly Nicene? Yes - for now. Subject to change
|
||||||
|
|
||||||
|
Would a Catholic-specific card be visible to an Orthodox user? Yes
|
||||||
|
Can users choose traditions? No - maybe they can change UI to match tradition (addon?)
|
||||||
|
Can users collect every tradition? Yes - I want people to also learn
|
||||||
|
|
||||||
|
What constitutes a unique card?
|
||||||
|
CardIdentity
|
||||||
|
Artwork
|
||||||
|
Rarity
|
||||||
|
Finish
|
||||||
|
Printing
|
||||||
|
Material
|
||||||
|
Wear - but in percentages so Mint might be like 1.00 - .95, Minimal Wear .95 - .75, etc etc,
|
||||||
|
|
||||||
|
Economy
|
||||||
|
* I think those can come in the future - Let's work on this area specifically
|
||||||
|
* Maybe we have a way to allow essentially destruction of cards - use up like 20 common cards to generate a uncommon. Similar to CounterStrike trade ups
|
||||||
|
* Allows for also removing things from the pool
|
||||||
|
|
||||||
|
Are pack probabilities globally fixed or dynamic?
|
||||||
|
* Fixed - maybe we wire in the variables to allow for dynamic in future if we feel the numbers aren't working as expected
|
||||||
|
|
||||||
|
What experience do you want someone to have the morning after they fail badly?
|
||||||
|
* We treat with grace. Tell them that we are excited to have them back and every journey begins with the first step
|
||||||
|
|
||||||
|
Structural change
|
||||||
|
* I like the structural change.
|
||||||
|
|
||||||
|
I like the idea for the technical spike of flutter scene versus Three.js
|
||||||
1086
functional-spec-v2.md
Normal file
BIN
image-1.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
image-2.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
image-3.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
image-4.png
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
845
sanctification-tcg/art-direction.md
Normal file
@@ -0,0 +1,845 @@
|
|||||||
|
# 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.
|
||||||
75
sanctification-tcg/blender_prototype/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Sanctification TCG — Blender card material spike
|
||||||
|
|
||||||
|
Open `sanctification_card_material_prototype.blend` in Blender 5.2+.
|
||||||
|
|
||||||
|
The file uses one shared physical mesh (`GEO_Card_Master_63x88`) across every card instance. Each object overrides three material slots: front, universal back, and edge/core. The source art remains separate from geometry.
|
||||||
|
|
||||||
|
## Main organization
|
||||||
|
|
||||||
|
- `Cards/`: one collection per finish/substrate variant.
|
||||||
|
- `Scene_Setup/`: neutral studio, labels, four lights, and inspection cameras.
|
||||||
|
- `NG_CardSurfaceTexture`: preserves and lightly grades printed art.
|
||||||
|
- `NG_CardMicroNormal`: paper fiber, restrained linen weave, and scratch response.
|
||||||
|
- `NG_CardWear`: seeded surface/edge wear mask.
|
||||||
|
- `NG_CardFoil`: art-masked, view-responsive foil enhancement.
|
||||||
|
- `NG_CardHolographic`: broad, angle-dependent spectral shift with art-derived masking.
|
||||||
|
- `NG_CardBase`: shared Principled BSDF assembly.
|
||||||
|
|
||||||
|
The holographic animation is keyed on `CARD_Holo_Demo` from frames 1–120 at 24 fps.
|
||||||
|
|
||||||
|
### Premium finish revision (v3)
|
||||||
|
|
||||||
|
The three David finish materials now use a printed-ink BSDF mixed with a masked metallic reflection. Foil retains the artwork color in both layers: the old gold Screen blend, diffuse rainbow mix, thin-film color outlines, and high-frequency groove bump are absent from these materials. Holo colors the reflective layer using broad UV gradients and the signed view direction in card coordinates. It is an artistic diffraction approximation, not a spectral optical simulation.
|
||||||
|
|
||||||
|
`NG_David_PrintProtection` combines saturation/ink selection with soft UV exclusions for the face, hand, lamb, and central title panel. Those exclusions are specific to `legendary.png`; another artwork needs adjusted regions or an authored mask.
|
||||||
|
|
||||||
|
Edit `Finish Strength` on each premium material (foil 0.56, holo 0.60, metal experiment 0.72), the reflective BSDF Roughness (foil 0.19, holo 0.23), and `Broad diffraction spectrum` in the holo material. `CardRenderConfig.*` properties are descriptive snapshots; they do not drive shader values. `premium_finishes.py` owns these defaults and can update the existing scene without rebuilding geometry or the wear shader. The full builder calls it automatically.
|
||||||
|
|
||||||
|
Use `render_premium_review.py` in Blender to regenerate the review stills; set `REVIEW_ANIMATION=True` in its execution globals for the 120 PNG animation frames. The MP4 is encoded externally from `renders/holo_frames/holo_%04d.png` at 24 fps. Preview files with `material_revision` or `lighting_preview` in their names are historical experiments; the standard filenames are the current deliverables.
|
||||||
|
|
||||||
|
## Runtime translation
|
||||||
|
|
||||||
|
Reasonable bake/export candidates:
|
||||||
|
|
||||||
|
- Base color
|
||||||
|
- Roughness and metallic maps
|
||||||
|
- Paper/linen micro-normal
|
||||||
|
- Static wear and imperfection masks
|
||||||
|
|
||||||
|
Likely custom runtime shader work:
|
||||||
|
|
||||||
|
- View-dependent foil
|
||||||
|
- Holographic diffraction/spectral shift
|
||||||
|
- Angle response and moving reflection behavior
|
||||||
|
|
||||||
|
The Blender node setup is a look-development reference; it is not expected to survive glTF export unchanged.
|
||||||
|
|
||||||
|
## Delivered renders
|
||||||
|
|
||||||
|
- `comparison_lineup.png`
|
||||||
|
- `timothy_matte_closeup.png`
|
||||||
|
- `timothy_linen_closeup.png`
|
||||||
|
- `david_paper_closeup.png`
|
||||||
|
- `david_foil_closeup.png`
|
||||||
|
- `david_holographic_head_on.png`
|
||||||
|
- `david_holographic_spectrum_angle.png`
|
||||||
|
- `card_three_quarter_thickness.png`
|
||||||
|
- `wear_test_timothy.png`
|
||||||
|
- `universal_card_back.png`
|
||||||
|
- `holographic_rotation_demo.mp4` (5 seconds, 120 frames, 24 fps)
|
||||||
|
|
||||||
|
The animation is authored on the hidden `CARD_Holo_Demo` linked instance, not the lineup card. Its final range is −18° to +12°: wide enough to show the changing surface response while avoiding a strip-light angle that made the printed title unreadable. Blender in this environment did not expose FFmpeg output internally, so Blender rendered the PNG sequence and the included MP4 was encoded from those exact frames with H.264.
|
||||||
|
|
||||||
|
The studio uses 3 W key, 1 W fill, 0.65 W strip, and 1.4 W rim lights, a 0.012-strength world, and −1.35 stop AgX exposure. The narrower strip is positioned at (0.10, 0.012, 0.23) m to catch the demo's tilt range. These settings are shared by the saved scene, review stills, and animation.
|
||||||
|
|
||||||
|
## Spike observations
|
||||||
|
|
||||||
|
- Paper, linen, and laminate differ mainly through roughness, coat, and micro-normal; the art texture does not change.
|
||||||
|
- Linen remains intentionally close-inspection detail. Increasing `Linen Strength` beyond the current setting begins to compete with the watercolor/stained-glass texture.
|
||||||
|
- Foil is most convincing when the saturated/value-derived art mask controls the metallic lift instead of applying reflection uniformly.
|
||||||
|
- Holo needs a runtime view vector plus broad directional bands. Static glTF export can carry its base PBR state, but not the authored spectral shift.
|
||||||
|
- `David_MetalHolo` deliberately marks the excessive end of the range and is not a recommended product target.
|
||||||
|
|
||||||
|
## Rebuild
|
||||||
|
|
||||||
|
The scene was constructed through Blender MCP using `build_sanctification_card_prototype.py`. Running the script again intentionally rebuilds the current scene from scratch.
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = "/home/dkzver/dev/sanctification-tcg"
|
||||||
|
OUT = os.path.join(ROOT, "blender_prototype", "renders")
|
||||||
|
BLEND = os.path.join(ROOT, "blender_prototype", "sanctification_card_material_prototype.blend")
|
||||||
|
COMMON = os.path.join(ROOT, "common.png")
|
||||||
|
LEGENDARY = os.path.join(ROOT, "legendary.png")
|
||||||
|
BACK = os.path.join(ROOT, "card-back.png")
|
||||||
|
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_scene():
|
||||||
|
if bpy.context.object and bpy.context.object.mode != "OBJECT":
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
for block in (bpy.data.collections, bpy.data.materials, bpy.data.meshes,
|
||||||
|
bpy.data.curves, bpy.data.cameras, bpy.data.lights,
|
||||||
|
bpy.data.node_groups):
|
||||||
|
for item in list(block):
|
||||||
|
if item.users == 0:
|
||||||
|
block.remove(item)
|
||||||
|
|
||||||
|
|
||||||
|
def collection(name, parent=None):
|
||||||
|
c = bpy.data.collections.new(name)
|
||||||
|
(parent or bpy.context.scene.collection).children.link(c)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def link_only(obj, col):
|
||||||
|
for c in list(obj.users_collection):
|
||||||
|
c.objects.unlink(obj)
|
||||||
|
col.objects.link(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def sock(node, name, fallback=None):
|
||||||
|
s = node.inputs.get(name)
|
||||||
|
if s is None and fallback is not None:
|
||||||
|
s = node.inputs[fallback]
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def new_socket(group, name, in_out, socket_type, default=None, min_value=None, max_value=None):
|
||||||
|
s = group.interface.new_socket(name=name, in_out=in_out, socket_type=socket_type)
|
||||||
|
if default is not None:
|
||||||
|
s.default_value = default
|
||||||
|
if min_value is not None:
|
||||||
|
s.min_value = min_value
|
||||||
|
if max_value is not None:
|
||||||
|
s.max_value = max_value
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def make_surface_texture_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardSurfaceTexture", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Art Color", "INPUT", "NodeSocketColor", (0.8, 0.8, 0.8, 1))
|
||||||
|
new_socket(g, "Saturation", "INPUT", "NodeSocketFloat", 1.0, 0.0, 2.0)
|
||||||
|
new_socket(g, "Value", "INPUT", "NodeSocketFloat", 1.0, 0.0, 2.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
n_in = g.nodes.new("NodeGroupInput")
|
||||||
|
n_out = g.nodes.new("NodeGroupOutput")
|
||||||
|
hsv = g.nodes.new("ShaderNodeHueSaturation")
|
||||||
|
hsv.inputs[0].default_value = 0.5
|
||||||
|
hsv.inputs[3].default_value = 1.0
|
||||||
|
g.links.new(n_in.outputs["Art Color"], hsv.inputs[4])
|
||||||
|
g.links.new(n_in.outputs["Saturation"], hsv.inputs[1])
|
||||||
|
g.links.new(n_in.outputs["Value"], hsv.inputs[2])
|
||||||
|
g.links.new(hsv.outputs[0], n_out.inputs["Color"])
|
||||||
|
n_in.location = (-260, 0)
|
||||||
|
hsv.location = (0, 0)
|
||||||
|
n_out.location = (220, 0)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_wear_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardWear", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor", (0.8, 0.8, 0.8, 1))
|
||||||
|
new_socket(g, "Vector", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Wear Amount", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Edge Wear Amount", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Seed", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1000.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Wear Mask", "OUTPUT", "NodeSocketFloat")
|
||||||
|
|
||||||
|
ni = g.nodes.new("NodeGroupInput")
|
||||||
|
no = g.nodes.new("NodeGroupOutput")
|
||||||
|
sep = g.nodes.new("ShaderNodeSeparateXYZ")
|
||||||
|
subx = g.nodes.new("ShaderNodeMath"); subx.operation = "SUBTRACT"; subx.inputs[1].default_value = 0.5
|
||||||
|
suby = g.nodes.new("ShaderNodeMath"); suby.operation = "SUBTRACT"; suby.inputs[1].default_value = 0.5
|
||||||
|
absx = g.nodes.new("ShaderNodeMath"); absx.operation = "ABSOLUTE"
|
||||||
|
absy = g.nodes.new("ShaderNodeMath"); absy.operation = "ABSOLUTE"
|
||||||
|
maximum = g.nodes.new("ShaderNodeMath"); maximum.operation = "MAXIMUM"
|
||||||
|
edge = g.nodes.new("ShaderNodeMapRange")
|
||||||
|
edge.inputs[1].default_value = 0.435
|
||||||
|
edge.inputs[2].default_value = 0.5
|
||||||
|
edge.inputs[3].default_value = 0.0
|
||||||
|
edge.inputs[4].default_value = 1.0
|
||||||
|
edge.clamp = True
|
||||||
|
combine = g.nodes.new("ShaderNodeCombineXYZ")
|
||||||
|
seed_scale = g.nodes.new("ShaderNodeMath"); seed_scale.operation = "MULTIPLY"; seed_scale.inputs[1].default_value = 0.137
|
||||||
|
addvec = g.nodes.new("ShaderNodeVectorMath"); addvec.operation = "ADD"
|
||||||
|
noise = g.nodes.new("ShaderNodeTexNoise")
|
||||||
|
noise.inputs["Scale"].default_value = 22.0
|
||||||
|
noise.inputs["Detail"].default_value = 5.0
|
||||||
|
noise.inputs["Roughness"].default_value = 0.72
|
||||||
|
mul_edge_noise = g.nodes.new("ShaderNodeMath"); mul_edge_noise.operation = "MULTIPLY"
|
||||||
|
mul_edge_amt = g.nodes.new("ShaderNodeMath"); mul_edge_amt.operation = "MULTIPLY"
|
||||||
|
fine_noise = g.nodes.new("ShaderNodeTexNoise")
|
||||||
|
fine_noise.inputs["Scale"].default_value = 135.0
|
||||||
|
fine_noise.inputs["Detail"].default_value = 2.0
|
||||||
|
fine_ramp = g.nodes.new("ShaderNodeValToRGB")
|
||||||
|
fine_ramp.color_ramp.elements[0].position = 0.61
|
||||||
|
fine_ramp.color_ramp.elements[1].position = 0.73
|
||||||
|
mul_surface = g.nodes.new("ShaderNodeMath"); mul_surface.operation = "MULTIPLY"
|
||||||
|
add_masks = g.nodes.new("ShaderNodeMath"); add_masks.operation = "ADD"; add_masks.use_clamp = True
|
||||||
|
mix = g.nodes.new("ShaderNodeMixRGB")
|
||||||
|
mix.blend_type = "MIX"
|
||||||
|
mix.inputs[2].default_value = (0.72, 0.68, 0.57, 1)
|
||||||
|
|
||||||
|
g.links.new(ni.outputs["Vector"], sep.inputs[0])
|
||||||
|
g.links.new(sep.outputs[0], subx.inputs[0]); g.links.new(subx.outputs[0], absx.inputs[0])
|
||||||
|
g.links.new(sep.outputs[1], suby.inputs[0]); g.links.new(suby.outputs[0], absy.inputs[0])
|
||||||
|
g.links.new(absx.outputs[0], maximum.inputs[0]); g.links.new(absy.outputs[0], maximum.inputs[1])
|
||||||
|
g.links.new(maximum.outputs[0], edge.inputs[0])
|
||||||
|
g.links.new(ni.outputs["Seed"], seed_scale.inputs[0])
|
||||||
|
g.links.new(seed_scale.outputs[0], combine.inputs[0]); g.links.new(ni.outputs["Seed"], combine.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], addvec.inputs[0]); g.links.new(combine.outputs[0], addvec.inputs[1])
|
||||||
|
g.links.new(addvec.outputs[0], noise.inputs["Vector"]); g.links.new(addvec.outputs[0], fine_noise.inputs["Vector"])
|
||||||
|
g.links.new(edge.outputs[0], mul_edge_noise.inputs[0]); g.links.new(noise.outputs["Fac"], mul_edge_noise.inputs[1])
|
||||||
|
g.links.new(mul_edge_noise.outputs[0], mul_edge_amt.inputs[0]); g.links.new(ni.outputs["Edge Wear Amount"], mul_edge_amt.inputs[1])
|
||||||
|
g.links.new(fine_noise.outputs["Fac"], fine_ramp.inputs[0])
|
||||||
|
g.links.new(fine_ramp.outputs[0], mul_surface.inputs[0]); g.links.new(ni.outputs["Wear Amount"], mul_surface.inputs[1])
|
||||||
|
g.links.new(mul_edge_amt.outputs[0], add_masks.inputs[0]); g.links.new(mul_surface.outputs[0], add_masks.inputs[1])
|
||||||
|
g.links.new(add_masks.outputs[0], mix.inputs[0]); g.links.new(ni.outputs["Base Color"], mix.inputs[1])
|
||||||
|
g.links.new(mix.outputs[0], no.inputs["Color"]); g.links.new(add_masks.outputs[0], no.inputs["Wear Mask"])
|
||||||
|
for i, n in enumerate(g.nodes):
|
||||||
|
n.location = ((i % 6) * 180 - 520, -(i // 6) * 180)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_micro_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardMicroNormal", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Vector", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Micro Texture Strength", "INPUT", "NodeSocketFloat", 0.08, 0.0, 1.0)
|
||||||
|
new_socket(g, "Linen Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Holo Groove Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Scratch Amount", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Seed", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1000.0)
|
||||||
|
new_socket(g, "Normal", "OUTPUT", "NodeSocketVector")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
noise = g.nodes.new("ShaderNodeTexNoise")
|
||||||
|
noise.inputs["Scale"].default_value = 470.0
|
||||||
|
noise.inputs["Detail"].default_value = 2.0
|
||||||
|
noise.inputs["Roughness"].default_value = 0.62
|
||||||
|
wave_x = g.nodes.new("ShaderNodeTexWave"); wave_x.wave_type = "BANDS"; wave_x.bands_direction = "X"
|
||||||
|
wave_y = g.nodes.new("ShaderNodeTexWave"); wave_y.wave_type = "BANDS"; wave_y.bands_direction = "Y"
|
||||||
|
for w in (wave_x, wave_y):
|
||||||
|
w.inputs["Scale"].default_value = 190.0
|
||||||
|
w.inputs["Distortion"].default_value = 0.8
|
||||||
|
w.inputs["Detail"].default_value = 2.0
|
||||||
|
weave = g.nodes.new("ShaderNodeMath"); weave.operation = "ADD"
|
||||||
|
weave_amt = g.nodes.new("ShaderNodeMath"); weave_amt.operation = "MULTIPLY"
|
||||||
|
paper_amt = g.nodes.new("ShaderNodeMath"); paper_amt.operation = "MULTIPLY"
|
||||||
|
add = g.nodes.new("ShaderNodeMath"); add.operation = "ADD"
|
||||||
|
scratch_wave = g.nodes.new("ShaderNodeTexWave"); scratch_wave.wave_type = "BANDS"; scratch_wave.bands_direction = "X"
|
||||||
|
scratch_wave.inputs["Scale"].default_value = 1250.0
|
||||||
|
scratch_wave.inputs["Distortion"].default_value = 8.0
|
||||||
|
scratch_ramp = g.nodes.new("ShaderNodeValToRGB")
|
||||||
|
scratch_ramp.color_ramp.elements[0].position = 0.48
|
||||||
|
scratch_ramp.color_ramp.elements[1].position = 0.505
|
||||||
|
scratch_amt = g.nodes.new("ShaderNodeMath"); scratch_amt.operation = "MULTIPLY"
|
||||||
|
add2 = g.nodes.new("ShaderNodeMath"); add2.operation = "ADD"
|
||||||
|
groove_wave = g.nodes.new("ShaderNodeTexWave"); groove_wave.wave_type = "BANDS"; groove_wave.bands_direction = "X"
|
||||||
|
groove_wave.inputs["Scale"].default_value = 900.0
|
||||||
|
groove_wave.inputs["Distortion"].default_value = 1.8
|
||||||
|
groove_wave.inputs["Detail"].default_value = 2.0
|
||||||
|
groove_amt = g.nodes.new("ShaderNodeMath"); groove_amt.operation = "MULTIPLY"
|
||||||
|
add3 = g.nodes.new("ShaderNodeMath"); add3.operation = "ADD"
|
||||||
|
bump = g.nodes.new("ShaderNodeBump")
|
||||||
|
bump.inputs["Distance"].default_value = 0.00015
|
||||||
|
bump.inputs["Strength"].default_value = 0.42
|
||||||
|
g.links.new(ni.outputs["Vector"], noise.inputs["Vector"])
|
||||||
|
g.links.new(ni.outputs["Vector"], wave_x.inputs["Vector"]); g.links.new(ni.outputs["Vector"], wave_y.inputs["Vector"])
|
||||||
|
g.links.new(wave_x.outputs["Color"], weave.inputs[0]); g.links.new(wave_y.outputs["Color"], weave.inputs[1])
|
||||||
|
g.links.new(weave.outputs[0], weave_amt.inputs[0]); g.links.new(ni.outputs["Linen Strength"], weave_amt.inputs[1])
|
||||||
|
g.links.new(noise.outputs["Fac"], paper_amt.inputs[0]); g.links.new(ni.outputs["Micro Texture Strength"], paper_amt.inputs[1])
|
||||||
|
g.links.new(weave_amt.outputs[0], add.inputs[0]); g.links.new(paper_amt.outputs[0], add.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], scratch_wave.inputs["Vector"]); g.links.new(scratch_wave.outputs["Color"], scratch_ramp.inputs[0])
|
||||||
|
g.links.new(scratch_ramp.outputs[0], scratch_amt.inputs[0]); g.links.new(ni.outputs["Scratch Amount"], scratch_amt.inputs[1])
|
||||||
|
g.links.new(add.outputs[0], add2.inputs[0]); g.links.new(scratch_amt.outputs[0], add2.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], groove_wave.inputs["Vector"]); g.links.new(groove_wave.outputs["Color"], groove_amt.inputs[0]); g.links.new(ni.outputs["Holo Groove Strength"], groove_amt.inputs[1])
|
||||||
|
g.links.new(add2.outputs[0], add3.inputs[0]); g.links.new(groove_amt.outputs[0], add3.inputs[1])
|
||||||
|
g.links.new(add3.outputs[0], bump.inputs["Height"]); g.links.new(bump.outputs["Normal"], no.inputs["Normal"])
|
||||||
|
for i, n in enumerate(g.nodes): n.location = ((i % 5) * 190 - 450, -(i // 5) * 190)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_foil_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardFoil", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Art Mask", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Metallic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Roughness", "INPUT", "NodeSocketFloat", 0.4, 0.0, 1.0)
|
||||||
|
new_socket(g, "Foil Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Foil Roughness", "INPUT", "NodeSocketFloat", 0.22, 0.0, 1.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Metallic", "OUTPUT", "NodeSocketFloat")
|
||||||
|
new_socket(g, "Roughness", "OUTPUT", "NodeSocketFloat")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
lw = g.nodes.new("ShaderNodeLayerWeight")
|
||||||
|
inv = g.nodes.new("ShaderNodeMath"); inv.operation = "SUBTRACT"; inv.inputs[0].default_value = 1.0
|
||||||
|
angle = g.nodes.new("ShaderNodeMath"); angle.operation = "MULTIPLY_ADD"; angle.inputs[1].default_value = 0.65; angle.inputs[2].default_value = 0.18
|
||||||
|
mask = g.nodes.new("ShaderNodeMath"); mask.operation = "MULTIPLY"
|
||||||
|
amt = g.nodes.new("ShaderNodeMath"); amt.operation = "MULTIPLY"
|
||||||
|
static_amt = g.nodes.new("ShaderNodeMath"); static_amt.operation = "MULTIPLY"
|
||||||
|
mix = g.nodes.new("ShaderNodeMixRGB"); mix.blend_type = "SCREEN"; mix.inputs[2].default_value = (0.74, 0.67, 0.48, 1)
|
||||||
|
metboost = g.nodes.new("ShaderNodeMath"); metboost.operation = "MULTIPLY"; metboost.inputs[1].default_value = 0.72
|
||||||
|
metadd = g.nodes.new("ShaderNodeMath"); metadd.operation = "ADD"; metadd.use_clamp = True
|
||||||
|
roughmix = g.nodes.new("ShaderNodeMix")
|
||||||
|
roughmix.data_type = "FLOAT"
|
||||||
|
g.links.new(lw.outputs["Facing"], inv.inputs[1]); g.links.new(lw.outputs["Facing"], angle.inputs[0])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], mask.inputs[0]); g.links.new(angle.outputs[0], mask.inputs[1])
|
||||||
|
g.links.new(mask.outputs[0], amt.inputs[0]); g.links.new(ni.outputs["Foil Strength"], amt.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], static_amt.inputs[0]); g.links.new(ni.outputs["Foil Strength"], static_amt.inputs[1])
|
||||||
|
g.links.new(amt.outputs[0], mix.inputs[0]); g.links.new(ni.outputs["Base Color"], mix.inputs[1])
|
||||||
|
g.links.new(static_amt.outputs[0], metboost.inputs[0]); g.links.new(metboost.outputs[0], metadd.inputs[0]); g.links.new(ni.outputs["Base Metallic"], metadd.inputs[1])
|
||||||
|
g.links.new(static_amt.outputs[0], roughmix.inputs[0]); g.links.new(ni.outputs["Base Roughness"], roughmix.inputs[2]); g.links.new(ni.outputs["Foil Roughness"], roughmix.inputs[3])
|
||||||
|
g.links.new(mix.outputs[0], no.inputs["Color"]); g.links.new(metadd.outputs[0], no.inputs["Metallic"]); g.links.new(roughmix.outputs[0], no.inputs["Roughness"])
|
||||||
|
for i, n in enumerate(g.nodes): n.location = ((i % 5) * 190 - 420, -(i // 5) * 190)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_holo_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardHolographic", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Vector", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Art Mask", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Metallic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Base Roughness", "INPUT", "NodeSocketFloat", 0.4, 0.0, 1.0)
|
||||||
|
new_socket(g, "Holo Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.5)
|
||||||
|
new_socket(g, "Holo Scale", "INPUT", "NodeSocketFloat", 5.0, 0.5, 30.0)
|
||||||
|
new_socket(g, "Holo Saturation", "INPUT", "NodeSocketFloat", 0.72, 0.0, 1.5)
|
||||||
|
new_socket(g, "Holo Angle Response", "INPUT", "NodeSocketFloat", 0.85, 0.0, 2.0)
|
||||||
|
new_socket(g, "Color", "OUTPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Metallic", "OUTPUT", "NodeSocketFloat")
|
||||||
|
new_socket(g, "Roughness", "OUTPUT", "NodeSocketFloat")
|
||||||
|
new_socket(g, "Thin Film Thickness", "OUTPUT", "NodeSocketFloat")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
lw = g.nodes.new("ShaderNodeLayerWeight")
|
||||||
|
inv = g.nodes.new("ShaderNodeMath"); inv.operation = "SUBTRACT"; inv.inputs[0].default_value = 1.0
|
||||||
|
power = g.nodes.new("ShaderNodeMath"); power.operation = "POWER"; power.inputs[1].default_value = 0.38
|
||||||
|
angle = g.nodes.new("ShaderNodeMath"); angle.operation = "MULTIPLY"
|
||||||
|
anglebase = g.nodes.new("ShaderNodeMath"); anglebase.operation = "ADD"; anglebase.inputs[1].default_value = 0.035
|
||||||
|
wave = g.nodes.new("ShaderNodeTexWave"); wave.wave_type = "BANDS"; wave.bands_direction = "X"
|
||||||
|
wave.inputs["Distortion"].default_value = 2.2; wave.inputs["Detail"].default_value = 3.0
|
||||||
|
rotate = g.nodes.new("ShaderNodeVectorRotate"); rotate.rotation_type = "AXIS_ANGLE"; rotate.inputs["Axis"].default_value = (0,0,1); rotate.inputs["Angle"].default_value = math.radians(33)
|
||||||
|
wave2 = g.nodes.new("ShaderNodeTexWave"); wave2.wave_type = "BANDS"; wave2.bands_direction = "X"; wave2.inputs["Distortion"].default_value = 1.1; wave2.inputs["Detail"].default_value = 2.0
|
||||||
|
scale2 = g.nodes.new("ShaderNodeMath"); scale2.operation = "MULTIPLY"; scale2.inputs[1].default_value = 0.63
|
||||||
|
wave1_amt = g.nodes.new("ShaderNodeMath"); wave1_amt.operation = "MULTIPLY"; wave1_amt.inputs[1].default_value = 0.72
|
||||||
|
wave2_amt = g.nodes.new("ShaderNodeMath"); wave2_amt.operation = "MULTIPLY"; wave2_amt.inputs[1].default_value = 0.28
|
||||||
|
waves_add = g.nodes.new("ShaderNodeMath"); waves_add.operation = "ADD"
|
||||||
|
noise = g.nodes.new("ShaderNodeTexNoise"); noise.inputs["Scale"].default_value = 3.2; noise.inputs["Detail"].default_value = 2.0
|
||||||
|
nscale = g.nodes.new("ShaderNodeMath"); nscale.operation = "MULTIPLY"; nscale.inputs[1].default_value = 0.16
|
||||||
|
fscale = g.nodes.new("ShaderNodeMath"); fscale.operation = "MULTIPLY"; fscale.inputs[1].default_value = 0.72
|
||||||
|
add1 = g.nodes.new("ShaderNodeMath"); add1.operation = "ADD"
|
||||||
|
add2 = g.nodes.new("ShaderNodeMath"); add2.operation = "ADD"
|
||||||
|
fract = g.nodes.new("ShaderNodeMath"); fract.operation = "FRACT"
|
||||||
|
ramp = g.nodes.new("ShaderNodeValToRGB")
|
||||||
|
cr = ramp.color_ramp
|
||||||
|
cr.interpolation = "EASE"
|
||||||
|
while len(cr.elements) > 2: cr.elements.remove(cr.elements[-1])
|
||||||
|
cr.elements[0].position = 0.0; cr.elements[0].color = (1.0, 0.34, 0.28, 1)
|
||||||
|
cr.elements[1].position = 1.0; cr.elements[1].color = (1.0, 0.34, 0.28, 1)
|
||||||
|
for pos, color in [
|
||||||
|
(0.17, (1.0, 0.76, 0.24, 1)), (0.34, (0.28, 0.95, 0.58, 1)),
|
||||||
|
(0.51, (0.20, 0.78, 1.0, 1)), (0.68, (0.38, 0.48, 1.0, 1)),
|
||||||
|
(0.85, (0.95, 0.32, 0.82, 1))]:
|
||||||
|
e = cr.elements.new(pos); e.color = color
|
||||||
|
hsv = g.nodes.new("ShaderNodeHueSaturation"); hsv.inputs[0].default_value = 0.5; hsv.inputs[2].default_value = 1.0; hsv.inputs[3].default_value = 1.0
|
||||||
|
holoamt = g.nodes.new("ShaderNodeMath"); holoamt.operation = "MULTIPLY"
|
||||||
|
holoamt2 = g.nodes.new("ShaderNodeMath"); holoamt2.operation = "MULTIPLY"
|
||||||
|
holo_cap = g.nodes.new("ShaderNodeMath"); holo_cap.operation = "MINIMUM"; holo_cap.inputs[1].default_value = 0.62
|
||||||
|
mix = g.nodes.new("ShaderNodeMixRGB"); mix.blend_type = "MIX"
|
||||||
|
metboost = g.nodes.new("ShaderNodeMath"); metboost.operation = "MULTIPLY"; metboost.inputs[1].default_value = 0.42
|
||||||
|
metadd = g.nodes.new("ShaderNodeMath"); metadd.operation = "ADD"; metadd.use_clamp = True
|
||||||
|
rough_target = g.nodes.new("ShaderNodeMath"); rough_target.operation = "MULTIPLY"; rough_target.inputs[1].default_value = 0.62
|
||||||
|
roughmix = g.nodes.new("ShaderNodeMix"); roughmix.data_type = "FLOAT"
|
||||||
|
film_mask = g.nodes.new("ShaderNodeMath"); film_mask.operation = "MULTIPLY"
|
||||||
|
film_scale = g.nodes.new("ShaderNodeMath"); film_scale.operation = "MULTIPLY"; film_scale.inputs[1].default_value = 460.0
|
||||||
|
g.links.new(lw.outputs["Facing"], inv.inputs[1]); g.links.new(lw.outputs["Facing"], power.inputs[0]); g.links.new(power.outputs[0], angle.inputs[0]); g.links.new(ni.outputs["Holo Angle Response"], angle.inputs[1]); g.links.new(angle.outputs[0], anglebase.inputs[0])
|
||||||
|
g.links.new(ni.outputs["Vector"], wave.inputs["Vector"]); g.links.new(ni.outputs["Holo Scale"], wave.inputs["Scale"])
|
||||||
|
g.links.new(ni.outputs["Vector"], rotate.inputs["Vector"]); g.links.new(rotate.outputs["Vector"], wave2.inputs["Vector"]); g.links.new(ni.outputs["Holo Scale"], scale2.inputs[0]); g.links.new(scale2.outputs[0], wave2.inputs["Scale"])
|
||||||
|
g.links.new(wave.outputs["Fac"], wave1_amt.inputs[0]); g.links.new(wave2.outputs["Fac"], wave2_amt.inputs[0]); g.links.new(wave1_amt.outputs[0], waves_add.inputs[0]); g.links.new(wave2_amt.outputs[0], waves_add.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Vector"], noise.inputs["Vector"]); g.links.new(noise.outputs["Fac"], nscale.inputs[0]); g.links.new(lw.outputs["Facing"], fscale.inputs[0])
|
||||||
|
g.links.new(waves_add.outputs[0], add1.inputs[0]); g.links.new(nscale.outputs[0], add1.inputs[1]); g.links.new(add1.outputs[0], add2.inputs[0]); g.links.new(fscale.outputs[0], add2.inputs[1]); g.links.new(add2.outputs[0], fract.inputs[0]); g.links.new(fract.outputs[0], ramp.inputs[0]); g.links.new(ramp.outputs[0], hsv.inputs[4]); g.links.new(ni.outputs["Holo Saturation"], hsv.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], holoamt.inputs[0]); g.links.new(anglebase.outputs[0], holoamt.inputs[1]); g.links.new(holoamt.outputs[0], holoamt2.inputs[0]); g.links.new(ni.outputs["Holo Strength"], holoamt2.inputs[1])
|
||||||
|
g.links.new(holoamt2.outputs[0], holo_cap.inputs[0]); g.links.new(holo_cap.outputs[0], mix.inputs[0]); g.links.new(ni.outputs["Base Color"], mix.inputs[1]); g.links.new(hsv.outputs[0], mix.inputs[2])
|
||||||
|
g.links.new(holo_cap.outputs[0], metboost.inputs[0]); g.links.new(metboost.outputs[0], metadd.inputs[0]); g.links.new(ni.outputs["Base Metallic"], metadd.inputs[1])
|
||||||
|
g.links.new(ni.outputs["Base Roughness"], rough_target.inputs[0]); g.links.new(holo_cap.outputs[0], roughmix.inputs[0]); g.links.new(ni.outputs["Base Roughness"], roughmix.inputs[2]); g.links.new(rough_target.outputs[0], roughmix.inputs[3])
|
||||||
|
g.links.new(ni.outputs["Art Mask"], film_mask.inputs[0]); g.links.new(ni.outputs["Holo Strength"], film_mask.inputs[1]); g.links.new(film_mask.outputs[0], film_scale.inputs[0])
|
||||||
|
g.links.new(mix.outputs[0], no.inputs["Color"]); g.links.new(metadd.outputs[0], no.inputs["Metallic"]); g.links.new(roughmix.outputs[0], no.inputs["Roughness"]); g.links.new(film_scale.outputs[0], no.inputs["Thin Film Thickness"])
|
||||||
|
for i, n in enumerate(g.nodes): n.location = ((i % 6) * 190 - 520, -(i // 6) * 190)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def make_base_group():
|
||||||
|
g = bpy.data.node_groups.new("NG_CardBase", "ShaderNodeTree")
|
||||||
|
new_socket(g, "Base Color", "INPUT", "NodeSocketColor")
|
||||||
|
new_socket(g, "Roughness", "INPUT", "NodeSocketFloat", 0.45, 0.0, 1.0)
|
||||||
|
new_socket(g, "Metallic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Coat Strength", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Coat Roughness", "INPUT", "NodeSocketFloat", 0.18, 0.0, 1.0)
|
||||||
|
new_socket(g, "Anisotropic", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Sheen Weight", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1.0)
|
||||||
|
new_socket(g, "Thin Film Thickness", "INPUT", "NodeSocketFloat", 0.0, 0.0, 1000.0)
|
||||||
|
new_socket(g, "Thin Film IOR", "INPUT", "NodeSocketFloat", 1.38, 1.0, 3.0)
|
||||||
|
new_socket(g, "Normal", "INPUT", "NodeSocketVector")
|
||||||
|
new_socket(g, "Shader", "OUTPUT", "NodeSocketShader")
|
||||||
|
ni = g.nodes.new("NodeGroupInput"); no = g.nodes.new("NodeGroupOutput")
|
||||||
|
bsdf = g.nodes.new("ShaderNodeBsdfPrincipled")
|
||||||
|
g.links.new(ni.outputs["Base Color"], sock(bsdf, "Base Color"))
|
||||||
|
g.links.new(ni.outputs["Roughness"], sock(bsdf, "Roughness"))
|
||||||
|
g.links.new(ni.outputs["Metallic"], sock(bsdf, "Metallic"))
|
||||||
|
g.links.new(ni.outputs["Coat Strength"], sock(bsdf, "Coat Weight", 18))
|
||||||
|
g.links.new(ni.outputs["Coat Roughness"], sock(bsdf, "Coat Roughness", 19))
|
||||||
|
g.links.new(ni.outputs["Anisotropic"], bsdf.inputs["Anisotropic"])
|
||||||
|
g.links.new(ni.outputs["Sheen Weight"], bsdf.inputs["Sheen Weight"])
|
||||||
|
g.links.new(ni.outputs["Thin Film Thickness"], bsdf.inputs["Thin Film Thickness"])
|
||||||
|
g.links.new(ni.outputs["Thin Film IOR"], bsdf.inputs["Thin Film IOR"])
|
||||||
|
g.links.new(ni.outputs["Normal"], sock(bsdf, "Normal"))
|
||||||
|
g.links.new(bsdf.outputs["BSDF"], no.inputs["Shader"])
|
||||||
|
ni.location = (-260, 0); bsdf.location = (0, 0); no.location = (260, 0)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def load_image(path, name):
|
||||||
|
img = bpy.data.images.load(path, check_existing=True)
|
||||||
|
img.name = name
|
||||||
|
img.colorspace_settings.name = "sRGB"
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def create_front_material(name, image, cfg, groups):
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
mat.use_nodes = True
|
||||||
|
mat.diffuse_color = (0.35, 0.3, 0.24, 1)
|
||||||
|
nt = mat.node_tree; nt.nodes.clear()
|
||||||
|
out = nt.nodes.new("ShaderNodeOutputMaterial"); out.location = (1050, 0)
|
||||||
|
texcoord = nt.nodes.new("ShaderNodeTexCoord"); texcoord.location = (-1100, -180)
|
||||||
|
tex = nt.nodes.new("ShaderNodeTexImage"); tex.image = image; tex.interpolation = "Linear"; tex.location = (-1100, 180); tex.label = image.name
|
||||||
|
surf = nt.nodes.new("ShaderNodeGroup"); surf.node_tree = groups["surface"]; surf.location = (-820, 180)
|
||||||
|
wear = nt.nodes.new("ShaderNodeGroup"); wear.node_tree = groups["wear"]; wear.location = (-570, 180)
|
||||||
|
sep = nt.nodes.new("ShaderNodeSeparateColor"); sep.mode = "HSV"; sep.location = (-810, -80)
|
||||||
|
maskmul = nt.nodes.new("ShaderNodeMath"); maskmul.operation = "MULTIPLY"; maskmul.location = (-575, -80)
|
||||||
|
maskramp = nt.nodes.new("ShaderNodeValToRGB"); maskramp.location = (-390, -80)
|
||||||
|
maskramp.color_ramp.elements[0].position = 0.20
|
||||||
|
maskramp.color_ramp.elements[1].position = 0.56
|
||||||
|
foil = nt.nodes.new("ShaderNodeGroup"); foil.node_tree = groups["foil"]; foil.location = (-105, 170)
|
||||||
|
holo = nt.nodes.new("ShaderNodeGroup"); holo.node_tree = groups["holo"]; holo.location = (170, 170)
|
||||||
|
micro = nt.nodes.new("ShaderNodeGroup"); micro.node_tree = groups["micro"]; micro.location = (170, -240)
|
||||||
|
base = nt.nodes.new("ShaderNodeGroup"); base.node_tree = groups["base"]; base.location = (640, 80)
|
||||||
|
surf.inputs["Saturation"].default_value = cfg.get("saturation", 1.0)
|
||||||
|
surf.inputs["Value"].default_value = cfg.get("value", 1.0)
|
||||||
|
wear.inputs["Wear Amount"].default_value = cfg.get("wear", 0.0)
|
||||||
|
wear.inputs["Edge Wear Amount"].default_value = cfg.get("edge_wear", 0.0)
|
||||||
|
wear.inputs["Seed"].default_value = cfg.get("seed", 1.0)
|
||||||
|
foil.inputs["Base Metallic"].default_value = cfg.get("metallic", 0.0)
|
||||||
|
foil.inputs["Base Roughness"].default_value = cfg.get("roughness", 0.5)
|
||||||
|
foil.inputs["Foil Strength"].default_value = cfg.get("foil", 0.0)
|
||||||
|
foil.inputs["Foil Roughness"].default_value = cfg.get("foil_roughness", 0.22)
|
||||||
|
holo.inputs["Holo Strength"].default_value = cfg.get("holo", 0.0)
|
||||||
|
holo.inputs["Holo Scale"].default_value = cfg.get("holo_scale", 5.0)
|
||||||
|
holo.inputs["Holo Saturation"].default_value = cfg.get("holo_saturation", 0.72)
|
||||||
|
holo.inputs["Holo Angle Response"].default_value = cfg.get("holo_angle", 0.85)
|
||||||
|
micro.inputs["Micro Texture Strength"].default_value = cfg.get("micro", 0.06)
|
||||||
|
micro.inputs["Linen Strength"].default_value = cfg.get("linen", 0.0)
|
||||||
|
micro.inputs["Holo Groove Strength"].default_value = cfg.get("holo_groove", 0.0)
|
||||||
|
micro.inputs["Scratch Amount"].default_value = cfg.get("scratches", 0.0)
|
||||||
|
micro.inputs["Seed"].default_value = cfg.get("seed", 1.0)
|
||||||
|
base.inputs["Coat Strength"].default_value = cfg.get("coat", 0.0)
|
||||||
|
base.inputs["Coat Roughness"].default_value = cfg.get("coat_roughness", 0.2)
|
||||||
|
base.inputs["Anisotropic"].default_value = cfg.get("anisotropic", 0.0)
|
||||||
|
base.inputs["Sheen Weight"].default_value = cfg.get("sheen", 0.0)
|
||||||
|
base.inputs["Thin Film IOR"].default_value = cfg.get("thin_film_ior", 1.38)
|
||||||
|
nt.links.new(texcoord.outputs["UV"], tex.inputs["Vector"]); nt.links.new(tex.outputs["Color"], surf.inputs["Art Color"])
|
||||||
|
nt.links.new(surf.outputs["Color"], wear.inputs["Base Color"]); nt.links.new(texcoord.outputs["Generated"], wear.inputs["Vector"])
|
||||||
|
nt.links.new(surf.outputs["Color"], sep.inputs["Color"]); nt.links.new(sep.outputs[1], maskmul.inputs[0]); nt.links.new(sep.outputs[2], maskmul.inputs[1]); nt.links.new(maskmul.outputs[0], maskramp.inputs[0])
|
||||||
|
nt.links.new(wear.outputs["Color"], foil.inputs["Base Color"]); nt.links.new(maskramp.outputs["Color"], foil.inputs["Art Mask"])
|
||||||
|
nt.links.new(foil.outputs["Color"], holo.inputs["Base Color"]); nt.links.new(texcoord.outputs["Generated"], holo.inputs["Vector"]); nt.links.new(maskramp.outputs["Color"], holo.inputs["Art Mask"]); nt.links.new(foil.outputs["Metallic"], holo.inputs["Base Metallic"]); nt.links.new(foil.outputs["Roughness"], holo.inputs["Base Roughness"])
|
||||||
|
nt.links.new(texcoord.outputs["Generated"], micro.inputs["Vector"])
|
||||||
|
nt.links.new(holo.outputs["Color"], base.inputs["Base Color"]); nt.links.new(holo.outputs["Metallic"], base.inputs["Metallic"]); nt.links.new(holo.outputs["Roughness"], base.inputs["Roughness"]); nt.links.new(holo.outputs["Thin Film Thickness"], base.inputs["Thin Film Thickness"]); nt.links.new(micro.outputs["Normal"], base.inputs["Normal"]); nt.links.new(base.outputs["Shader"], out.inputs["Surface"])
|
||||||
|
for k, v in cfg.items():
|
||||||
|
if isinstance(v, (int, float, str, bool)):
|
||||||
|
mat["CardRenderConfig." + k] = v
|
||||||
|
mat["CardRenderConfig.frontTexture"] = image.filepath
|
||||||
|
mat["CardRenderConfig.backTexture"] = BACK
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def create_simple_texture_material(name, image, roughness, coat, groups):
|
||||||
|
return create_front_material(name, image, dict(roughness=roughness, coat=coat, micro=0.04, seed=11), groups)
|
||||||
|
|
||||||
|
|
||||||
|
def create_edge_material(name, color, roughness, metallic=0.0):
|
||||||
|
m = bpy.data.materials.new(name); m.use_nodes = True
|
||||||
|
p = m.node_tree.nodes.get("Principled BSDF")
|
||||||
|
p.inputs["Base Color"].default_value = (*color, 1)
|
||||||
|
p.inputs["Roughness"].default_value = roughness
|
||||||
|
p.inputs["Metallic"].default_value = metallic
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def rounded_card_mesh():
|
||||||
|
w, h, t, r, seg = 0.063, 0.0882, 0.00040, 0.0028, 8
|
||||||
|
pts = []
|
||||||
|
for cx, cy, a0 in [
|
||||||
|
(w/2-r, h/2-r, 0), (-w/2+r, h/2-r, math.pi/2),
|
||||||
|
(-w/2+r, -h/2+r, math.pi), (w/2-r, -h/2+r, 3*math.pi/2)]:
|
||||||
|
for i in range(seg + 1):
|
||||||
|
a = a0 + (math.pi/2) * i / seg
|
||||||
|
pts.append((cx + r*math.cos(a), cy + r*math.sin(a)))
|
||||||
|
n = len(pts)
|
||||||
|
verts = [(x,y,-t/2) for x,y in pts] + [(x,y,t/2) for x,y in pts]
|
||||||
|
faces = [list(reversed(range(n))), list(range(n, 2*n))]
|
||||||
|
for i in range(n):
|
||||||
|
j = (i+1) % n
|
||||||
|
faces.append([i, j, n+j, n+i])
|
||||||
|
mesh = bpy.data.meshes.new("GEO_Card_Master_63x88")
|
||||||
|
mesh.from_pydata(verts, [], faces); mesh.update()
|
||||||
|
mesh.materials.append(None); mesh.materials.append(None); mesh.materials.append(None)
|
||||||
|
mesh.polygons[0].material_index = 1
|
||||||
|
mesh.polygons[1].material_index = 0
|
||||||
|
for p in mesh.polygons[2:]: p.material_index = 2
|
||||||
|
uv = mesh.uv_layers.new(name="UV_Card_Artwork")
|
||||||
|
for p in mesh.polygons:
|
||||||
|
for li in p.loop_indices:
|
||||||
|
vi = mesh.loops[li].vertex_index
|
||||||
|
x,y,z = mesh.vertices[vi].co
|
||||||
|
if p.index == 0:
|
||||||
|
uv.data[li].uv = (0.5-x/w, y/h+0.5)
|
||||||
|
elif p.index == 1:
|
||||||
|
uv.data[li].uv = (x/w+0.5, y/h+0.5)
|
||||||
|
else:
|
||||||
|
uv.data[li].uv = ((vi % n)/n, 0 if vi < n else 1)
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
def add_card(name, mesh, col, front_mat, back_mat, edge_mat, loc, rot):
|
||||||
|
obj = bpy.data.objects.new(name, mesh)
|
||||||
|
col.objects.link(obj)
|
||||||
|
obj.location = loc; obj.rotation_euler = rot
|
||||||
|
for idx, mat in enumerate((front_mat, back_mat, edge_mat)):
|
||||||
|
slot = obj.material_slots[idx]
|
||||||
|
slot.link = "OBJECT"; slot.material = mat
|
||||||
|
bev = obj.modifiers.new("MOD_EdgeHighlight_Bevel", "BEVEL")
|
||||||
|
bev.width = 0.00028; bev.segments = 2; bev.limit_method = "ANGLE"
|
||||||
|
obj["CardRenderConfig.frontMaterial"] = front_mat.name
|
||||||
|
obj["CardRenderConfig.backMaterial"] = back_mat.name
|
||||||
|
obj["Physical.Width_mm"] = 63.0; obj["Physical.Height_mm"] = 88.2; obj["Physical.Thickness_mm"] = 0.4; obj["Physical.CornerRadius_mm"] = 2.8
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def look_at(obj, target):
|
||||||
|
obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def add_area(name, loc, energy, size, color, target, col, shape="DISK", size_y=None):
|
||||||
|
d = bpy.data.lights.new(name, "AREA"); d.energy = energy; d.shape = shape; d.size = size; d.color = color
|
||||||
|
if size_y is not None: d.size_y = size_y
|
||||||
|
o = bpy.data.objects.new(name, d); col.objects.link(o); o.location = loc; look_at(o, target); return o
|
||||||
|
|
||||||
|
|
||||||
|
def add_camera(name, loc, target, col, ortho=None, lens=55):
|
||||||
|
d = bpy.data.cameras.new(name); d.lens = lens
|
||||||
|
if ortho is not None: d.type = "ORTHO"; d.ortho_scale = ortho
|
||||||
|
o = bpy.data.objects.new(name, d); col.objects.link(o); o.location = loc; look_at(o, target); return o
|
||||||
|
|
||||||
|
|
||||||
|
def make_label(text, loc, col, mat):
|
||||||
|
curve = bpy.data.curves.new("TXT_" + text.replace(" ", "_"), "FONT")
|
||||||
|
curve.body = text; curve.align_x = "CENTER"; curve.align_y = "CENTER"; curve.size = 0.006; curve.extrude = 0.00002
|
||||||
|
obj = bpy.data.objects.new("LABEL_" + text.replace(" ", "_"), curve); col.objects.link(obj); obj.location = loc; obj.data.materials.append(mat); return obj
|
||||||
|
|
||||||
|
|
||||||
|
clear_scene()
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.unit_settings.system = "METRIC"; scene.unit_settings.length_unit = "MILLIMETERS"; scene.unit_settings.scale_length = 1.0
|
||||||
|
scene.render.engine = "BLENDER_EEVEE"
|
||||||
|
scene.render.resolution_x = 1280; scene.render.resolution_y = 900; scene.render.resolution_percentage = 100
|
||||||
|
scene.render.image_settings.file_format = "PNG"
|
||||||
|
scene.render.film_transparent = False
|
||||||
|
scene.render.image_settings.color_mode = "RGBA"
|
||||||
|
scene.render.image_settings.color_depth = "8"
|
||||||
|
scene.render.fps = 24; scene.frame_start = 1; scene.frame_end = 120
|
||||||
|
scene.view_settings.look = "AgX - Medium High Contrast"
|
||||||
|
scene.view_settings.exposure = -1.35
|
||||||
|
scene.world.color = (0.008, 0.009, 0.012)
|
||||||
|
world = scene.world; world.use_nodes = True
|
||||||
|
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.008, 0.010, 0.014, 1)
|
||||||
|
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.012
|
||||||
|
|
||||||
|
cards_root = collection("Cards")
|
||||||
|
setup = collection("Scene_Setup")
|
||||||
|
lights_col = collection("Lights", setup); cams_col = collection("Cameras", setup); labels_col = collection("Labels", setup); env_col = collection("Environment", setup)
|
||||||
|
|
||||||
|
groups = {"surface": make_surface_texture_group(), "wear": make_wear_group(), "micro": make_micro_group(), "foil": make_foil_group(), "holo": make_holo_group(), "base": make_base_group()}
|
||||||
|
img_common = load_image(COMMON, "TEX_Timothy_Front")
|
||||||
|
img_legendary = load_image(LEGENDARY, "TEX_David_Front")
|
||||||
|
img_back = load_image(BACK, "TEX_Card_Back")
|
||||||
|
|
||||||
|
cfgs = {
|
||||||
|
"Timothy_Matte": (img_common, dict(substrate="paper", finish="matte", roughness=0.61, metallic=0.0, coat=0.02, coat_roughness=0.36, micro=0.08, linen=0.0, sheen=0.0, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=13)),
|
||||||
|
"Timothy_Linen": (img_common, dict(substrate="linen", finish="matte", roughness=0.77, metallic=0.0, coat=0.0, micro=0.045, linen=0.38, sheen=0.16, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=23)),
|
||||||
|
"Timothy_Gloss": (img_common, dict(substrate="plastic_laminate", finish="gloss", roughness=0.29, metallic=0.0, coat=0.48, coat_roughness=0.13, micro=0.015, linen=0.0, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=31)),
|
||||||
|
"David_Paper": (img_legendary, dict(substrate="premium_paper", finish="satin", roughness=0.47, metallic=0.0, coat=0.14, coat_roughness=0.24, micro=0.055, linen=0.0, foil=0.0, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=41)),
|
||||||
|
"David_Foil": (img_legendary, dict(substrate="foil_laminate", finish="selective_foil", roughness=0.24, metallic=0.04, coat=0.44, coat_roughness=0.09, anisotropic=0.55, micro=0.018, linen=0.0, foil=0.90, foil_roughness=0.075, holo=0.0, wear=0.0, edge_wear=0.0, scratches=0.0, seed=53)),
|
||||||
|
"David_Holo": (img_legendary, dict(substrate="premium_cardstock", finish="layered_holographic", roughness=0.17, metallic=0.02, coat=0.80, coat_roughness=0.055, anisotropic=0.68, micro=0.012, linen=0.0, holo_groove=0.035, foil=0.22, foil_roughness=0.085, holo=1.20, holo_scale=3.7, holo_saturation=0.82, holo_angle=1.48, thin_film_ior=1.42, wear=0.0, edge_wear=0.0, scratches=0.0, seed=67)),
|
||||||
|
"David_MetalHolo": (img_legendary, dict(substrate="metal", finish="layered_holographic", roughness=0.13, metallic=0.66, coat=0.78, coat_roughness=0.045, anisotropic=0.80, micro=0.008, linen=0.0, holo_groove=0.06, foil=0.78, foil_roughness=0.055, holo=1.45, holo_scale=4.8, holo_saturation=1.05, holo_angle=1.40, thin_film_ior=1.48, wear=0.0, edge_wear=0.0, scratches=0.0, seed=79)),
|
||||||
|
"Wear_Test": (img_common, dict(substrate="paper", finish="matte_worn", roughness=0.67, metallic=0.0, coat=0.01, coat_roughness=0.4, micro=0.10, linen=0.0, foil=0.0, holo=0.0, wear=0.11, edge_wear=0.60, scratches=0.14, seed=137)),
|
||||||
|
}
|
||||||
|
materials = {k: create_front_material("MAT_" + k, img, cfg, groups) for k, (img, cfg) in cfgs.items()}
|
||||||
|
back_mat = create_simple_texture_material("MAT_Card_Back_Universal", img_back, 0.44, 0.12, groups)
|
||||||
|
edge_paper = create_edge_material("MAT_Edge_PaperCore", (0.55, 0.48, 0.36), 0.72)
|
||||||
|
edge_plastic = create_edge_material("MAT_Edge_Plastic", (0.20, 0.22, 0.23), 0.30)
|
||||||
|
edge_metal = create_edge_material("MAT_Edge_Metal", (0.32, 0.24, 0.10), 0.18, 0.78)
|
||||||
|
|
||||||
|
mesh = rounded_card_mesh()
|
||||||
|
layout = {
|
||||||
|
"Timothy_Matte": ((-0.082, 0.061, 0), (math.radians(-2), math.radians(6), math.radians(-1.5))),
|
||||||
|
"Timothy_Linen": ((0.0, 0.061, 0), (math.radians(2), math.radians(-4), 0)),
|
||||||
|
"Timothy_Gloss": ((0.082, 0.061, 0), (math.radians(-3), math.radians(8), math.radians(1.5))),
|
||||||
|
"David_Paper": ((-0.123, -0.061, 0), (math.radians(2), math.radians(-6), math.radians(-1.5))),
|
||||||
|
"David_Foil": ((-0.041, -0.061, 0), (math.radians(-2), math.radians(8), math.radians(0.5))),
|
||||||
|
"David_Holo": ((0.041, -0.061, 0), (math.radians(3), math.radians(-11), math.radians(-0.5))),
|
||||||
|
"David_MetalHolo": ((0.123, -0.061, 0), (math.radians(-4), math.radians(13), math.radians(1.5))),
|
||||||
|
"Wear_Test": ((0.0, -0.185, 0), (0, 0, 0)),
|
||||||
|
}
|
||||||
|
objects = {}
|
||||||
|
for key, (loc, rot) in layout.items():
|
||||||
|
col = collection(key, cards_root)
|
||||||
|
edge = edge_metal if key == "David_MetalHolo" else edge_plastic if key == "Timothy_Gloss" else edge_paper
|
||||||
|
objects[key] = add_card("CARD_" + key, mesh, col, materials[key], back_mat, edge, loc, rot)
|
||||||
|
objects["Wear_Test"].hide_render = True
|
||||||
|
holo_demo_col = collection("Holo_Demo", cards_root)
|
||||||
|
holo_demo = add_card("CARD_Holo_Demo", mesh, holo_demo_col, materials["David_Holo"], back_mat, edge_paper, (0,0,0), (0,0,0))
|
||||||
|
holo_demo.hide_render = True
|
||||||
|
|
||||||
|
label_mat = bpy.data.materials.new("MAT_Label_White"); label_mat.use_nodes = True
|
||||||
|
lp = label_mat.node_tree.nodes.get("Principled BSDF"); lp.inputs["Base Color"].default_value = (0.72,0.75,0.80,1); lp.inputs["Roughness"].default_value = 0.65; lp.inputs["Emission Color"].default_value = (0.035,0.04,0.05,1); lp.inputs["Emission Strength"].default_value = 0.25
|
||||||
|
for key in ["Timothy_Matte", "Timothy_Linen", "Timothy_Gloss", "David_Paper", "David_Foil", "David_Holo", "David_MetalHolo"]:
|
||||||
|
x,y,_ = layout[key][0]
|
||||||
|
make_label(key.replace("_", " "), (x, y-0.051, 0.0015), labels_col, label_mat)
|
||||||
|
|
||||||
|
bg_mat = bpy.data.materials.new("MAT_Studio_Backdrop"); bg_mat.use_nodes = True
|
||||||
|
bp = bg_mat.node_tree.nodes.get("Principled BSDF"); bp.inputs["Base Color"].default_value = (0.012,0.015,0.022,1); bp.inputs["Roughness"].default_value = 0.82
|
||||||
|
bpy.ops.mesh.primitive_plane_add(size=2.0, location=(0,0,-0.012))
|
||||||
|
backdrop = bpy.context.object; backdrop.name = "Studio_Backdrop"; link_only(backdrop, env_col); backdrop.data.materials.append(bg_mat)
|
||||||
|
|
||||||
|
add_area("LIGHT_Key_Softbox", (-0.20,0.19,0.27), 3.0, 0.16, (1.0,0.92,0.82), (0,0,0), lights_col, "DISK")
|
||||||
|
add_area("LIGHT_Fill_Soft", (0.18,-0.14,0.22), 1.0, 0.20, (0.80,0.88,1.0), (0,0,0), lights_col, "DISK")
|
||||||
|
add_area("LIGHT_Specular_Strip", (0.24,0.10,0.16), 3.0, 0.018, (1.0,0.98,0.94), (0.025,-0.02,0), lights_col, "RECTANGLE", 0.23)
|
||||||
|
add_area("LIGHT_Top_Rim", (0.0,0.31,0.12), 1.4, 0.035, (0.90,0.94,1.0), (0,0.03,0), lights_col, "RECTANGLE", 0.20)
|
||||||
|
|
||||||
|
cam_comp = add_camera("CAM_Comparison", (0,0,0.48), (0,0,0), cams_col, ortho=0.335)
|
||||||
|
cam_front = add_camera("CAM_Front_Inspection", (0,0,0.28), (0,0,0), cams_col, ortho=0.108)
|
||||||
|
cam_three = add_camera("CAM_ThreeQuarter", (0.14,-0.040,0.28), (0,0,0), cams_col, lens=66)
|
||||||
|
cam_holo_angle = add_camera("CAM_Holo_Angle", (0.105,-0.025,0.27), (0,0,0), cams_col, lens=72)
|
||||||
|
scene.camera = cam_comp
|
||||||
|
|
||||||
|
# Holographic demo keys live on a dedicated linked instance, leaving the lineup
|
||||||
|
# card deterministic for head-on material comparisons.
|
||||||
|
holo_obj = holo_demo
|
||||||
|
holo_obj.rotation_euler = (math.radians(-6), math.radians(-18), 0); holo_obj.keyframe_insert("rotation_euler", frame=1)
|
||||||
|
holo_obj.rotation_euler = (math.radians(5), math.radians(12), math.radians(1.5)); holo_obj.keyframe_insert("rotation_euler", frame=120)
|
||||||
|
# Blender 5 uses layered Actions; the default Bezier interpolation is intentionally
|
||||||
|
# retained so the inspection motion eases naturally at each end.
|
||||||
|
scene.frame_set(1)
|
||||||
|
|
||||||
|
scene["README_RuntimeTranslation"] = "Bake/export: base color, roughness, metallic, micro-normal, static wear/imperfection masks. Runtime shader: view-dependent foil, holographic diffraction, angle-based spectrum, dynamic reflection."
|
||||||
|
scene["README_PrototypeIntent"] = "Rarity remains artwork/framing; finish remains independent physical light response. Shared mesh GEO_Card_Master_63x88 and shared NG_* node groups support parameterized variants."
|
||||||
|
scene["CardRenderConfig_Schema"] = "frontTexture, backTexture, substrate, finish, roughness, metallic, foil, holo, wear, seed"
|
||||||
|
|
||||||
|
import runpy
|
||||||
|
runpy.run_path(os.path.join(ROOT, 'blender_prototype', 'premium_finishes.py'))['apply_premium_finishes']()
|
||||||
|
runpy.run_path(os.path.join(ROOT, 'blender_prototype', 'timothy_linen_foil.py'))['add_timothy_linen_foil']()
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=BLEND)
|
||||||
|
result = {"status": "built", "blend": BLEND, "objects": list(objects.keys()), "materials": list(materials.keys()), "node_groups": list(groups.keys()), "output_dir": OUT}
|
||||||
156
sanctification-tcg/blender_prototype/card_finish_material.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
"""Artwork-independent foil/holo materials. No portrait or layout coordinates.
|
||||||
|
|
||||||
|
Call create_card_finish_material with a bpy Image and an optional grayscale mask.
|
||||||
|
Mask values: 0 = printed ink only, 1 = maximum requested finish coverage.
|
||||||
|
The default automatic mask is a color heuristic, not semantic segmentation.
|
||||||
|
"""
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def socket(g, name, kind, direction='INPUT', default=None):
|
||||||
|
s=g.interface.new_socket(name=name,in_out=direction,socket_type=kind)
|
||||||
|
if default is not None:s.default_value=default
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def node(t, kind, name):
|
||||||
|
n=t.nodes.new(kind);n.name=n.label=name
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def op(t, operation, a, b=0, name=None):
|
||||||
|
n=node(t,'ShaderNodeMath',name or operation);n.operation=operation
|
||||||
|
for i,v in enumerate((a,b)):
|
||||||
|
if isinstance(v,(int,float)):n.inputs[i].default_value=v
|
||||||
|
else:t.links.new(v,n.inputs[i])
|
||||||
|
return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def smooth(t,value,lo,hi,name):
|
||||||
|
n=node(t,'ShaderNodeMapRange',name);n.clamp=True;n.interpolation_type='SMOOTHSTEP'
|
||||||
|
n.inputs['From Min'].default_value=lo;n.inputs['From Max'].default_value=hi
|
||||||
|
t.links.new(value,n.inputs['Value']);return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def auto_mask_group():
|
||||||
|
name='NG_CardFinish_AutomaticMask'
|
||||||
|
if name in bpy.data.node_groups:return bpy.data.node_groups[name]
|
||||||
|
g=bpy.data.node_groups.new(name,'ShaderNodeTree')
|
||||||
|
socket(g,'Artwork','NodeSocketColor')
|
||||||
|
socket(g,'Mask','NodeSocketFloat','OUTPUT')
|
||||||
|
i=node(g,'NodeGroupInput','Artwork');o=node(g,'NodeGroupOutput','Color based coverage')
|
||||||
|
hsv=node(g,'ShaderNodeSeparateColor','Artwork saturation and value');hsv.mode='HSV'
|
||||||
|
g.links.new(i.outputs[0],hsv.inputs[0])
|
||||||
|
saturation=smooth(g,hsv.outputs[1],.08,.58,'Favor saturated color')
|
||||||
|
value=smooth(g,hsv.outputs[2],.015,.16,'Reduce finish over dark ink')
|
||||||
|
g.links.new(op(g,'MULTIPLY',saturation,value),o.inputs[0])
|
||||||
|
for index,n in enumerate(g.nodes):n.location=(index*200,0)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def finish_group():
|
||||||
|
name='NG_CardFinish_ReflectiveCoating'
|
||||||
|
if name in bpy.data.node_groups:return bpy.data.node_groups[name]
|
||||||
|
g=bpy.data.node_groups.new(name,'ShaderNodeTree')
|
||||||
|
for nm,kind,default in [('Artwork','NodeSocketColor',None),('UV','NodeSocketVector',None),
|
||||||
|
('Normal','NodeSocketVector',None),('Coverage','NodeSocketFloat',1.0),
|
||||||
|
('Finish Strength','NodeSocketFloat',.56),('Finish Roughness','NodeSocketFloat',.19),
|
||||||
|
('Ink Roughness','NodeSocketFloat',.36),('Anisotropy','NodeSocketFloat',.32),
|
||||||
|
('Holographic','NodeSocketFloat',0.0),('Sheen','NodeSocketFloat',0.0)]:
|
||||||
|
socket(g,nm,kind,default=default)
|
||||||
|
socket(g,'Shader','NodeSocketShader','OUTPUT')
|
||||||
|
i=node(g,'NodeGroupInput','CardRenderConfig');o=node(g,'NodeGroupOutput','Card surface')
|
||||||
|
ink=node(g,'ShaderNodeBsdfPrincipled','Printed ink')
|
||||||
|
g.links.new(i.outputs['Artwork'],ink.inputs['Base Color']);g.links.new(i.outputs['Normal'],ink.inputs['Normal'])
|
||||||
|
g.links.new(i.outputs['Ink Roughness'],ink.inputs['Roughness']);g.links.new(i.outputs['Sheen'],ink.inputs['Sheen Weight'])
|
||||||
|
ink.inputs['Specular IOR Level'].default_value=.16
|
||||||
|
coat=node(g,'ShaderNodeBsdfPrincipled','Reflective coating')
|
||||||
|
coat.inputs['Metallic'].default_value=1
|
||||||
|
g.links.new(i.outputs['Finish Roughness'],coat.inputs['Roughness']);g.links.new(i.outputs['Normal'],coat.inputs['Normal'])
|
||||||
|
g.links.new(i.outputs['Anisotropy'],coat.inputs['Anisotropic'])
|
||||||
|
# Tangent from UV gradients is provided by Blender; the mesh uses its active UV.
|
||||||
|
tangent=node(g,'ShaderNodeTangent','Surface direction');tangent.direction_type='UV_MAP'
|
||||||
|
g.links.new(tangent.outputs[0],coat.inputs['Tangent'])
|
||||||
|
geo=node(g,'ShaderNodeNewGeometry','Incoming view')
|
||||||
|
transform=node(g,'ShaderNodeVectorTransform','View in card space')
|
||||||
|
transform.vector_type='VECTOR';transform.convert_from='WORLD';transform.convert_to='OBJECT'
|
||||||
|
g.links.new(geo.outputs['Incoming'],transform.inputs[0])
|
||||||
|
v=node(g,'ShaderNodeSeparateXYZ','Signed view');g.links.new(transform.outputs[0],v.inputs[0])
|
||||||
|
uv=node(g,'ShaderNodeSeparateXYZ','Spatial phase');g.links.new(i.outputs['UV'],uv.inputs[0])
|
||||||
|
phase=op(g,'ADD',op(g,'MULTIPLY',v.outputs[0],2.8),op(g,'MULTIPLY',v.outputs[1],1.6))
|
||||||
|
phase=op(g,'ADD',phase,op(g,'MULTIPLY',uv.outputs[0],.65))
|
||||||
|
phase=op(g,'ADD',phase,op(g,'MULTIPLY',uv.outputs[1],.25))
|
||||||
|
phase=op(g,'FRACT',op(g,'ADD',phase,.3))
|
||||||
|
spectrum=node(g,'ShaderNodeValToRGB','Broad reflection spectrum')
|
||||||
|
colors=[(0,(1,.32,.12,1)),(.2,(1,.84,.24,1)),(.4,(.12,1,.57,1)),(.6,(.12,.6,1,1)),(.8,(.68,.24,1,1)),(1,(1,.32,.12,1))]
|
||||||
|
spectrum.color_ramp.elements[0].color=colors[0][1];spectrum.color_ramp.elements[-1].color=colors[-1][1]
|
||||||
|
for p,c in colors[1:-1]:spectrum.color_ramp.elements.new(p).color=c
|
||||||
|
g.links.new(phase,spectrum.inputs[0])
|
||||||
|
color=node(g,'ShaderNodeMixRGB','Foil or spectral reflection')
|
||||||
|
g.links.new(i.outputs['Holographic'],color.inputs[0]);g.links.new(i.outputs['Artwork'],color.inputs[1]);g.links.new(spectrum.outputs[0],color.inputs[2])
|
||||||
|
g.links.new(color.outputs[0],coat.inputs['Base Color'])
|
||||||
|
facing=node(g,'ShaderNodeLayerWeight','Tilt activation')
|
||||||
|
angle=op(g,'ADD',.1,op(g,'MULTIPLY',smooth(g,facing.outputs['Facing'],.015,.38,'Holo tilt response'),.9))
|
||||||
|
# Lerp between constant foil coverage and view-gated holo coverage.
|
||||||
|
gate=op(g,'ADD',1,op(g,'MULTIPLY',i.outputs['Holographic'],op(g,'SUBTRACT',angle,1)))
|
||||||
|
coverage=op(g,'MULTIPLY',i.outputs['Coverage'],i.outputs['Finish Strength'])
|
||||||
|
coverage=op(g,'MINIMUM',1,op(g,'MAXIMUM',0,op(g,'MULTIPLY',coverage,gate)))
|
||||||
|
mix=node(g,'ShaderNodeMixShader','Ink plus finish')
|
||||||
|
g.links.new(coverage,mix.inputs[0]);g.links.new(ink.outputs[0],mix.inputs[1]);g.links.new(coat.outputs[0],mix.inputs[2]);g.links.new(mix.outputs[0],o.inputs[0])
|
||||||
|
for index,n in enumerate(g.nodes):n.location=((index%7)*220,-(index//7)*220)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def create_card_finish_material(name, artwork, *, finish='foil', surface_texture='smooth',
|
||||||
|
finish_mask=None, mask_mode='automatic', strength=None, roughness=None, linen_strength=.38):
|
||||||
|
"""Create/update a material; no scene, object, camera, or light mutations.
|
||||||
|
|
||||||
|
artwork / finish_mask: bpy.types.Image. Optional masks must be Non-Color data.
|
||||||
|
finish: foil | holographic. surface_texture: smooth | paper | linen.
|
||||||
|
mask_mode: automatic | full. An explicit mask takes precedence.
|
||||||
|
Updates a named material in place: its existing users see the new settings.
|
||||||
|
"""
|
||||||
|
if finish not in {'foil','holographic'}:raise ValueError('Unknown finish')
|
||||||
|
if surface_texture not in {'smooth','paper','linen'}:raise ValueError('Unknown surface texture')
|
||||||
|
if mask_mode not in {'automatic','full'}:raise ValueError('Unknown mask mode')
|
||||||
|
if not isinstance(artwork,bpy.types.Image):raise TypeError('artwork must be a Blender image')
|
||||||
|
if finish_mask is not None and finish_mask.colorspace_settings.name!='Non-Color':
|
||||||
|
raise ValueError('Load finish_mask as Non-Color data')
|
||||||
|
if surface_texture!='smooth' and 'NG_CardMicroNormal' not in bpy.data.node_groups:
|
||||||
|
raise RuntimeError('Load/build NG_CardMicroNormal before using paper or linen')
|
||||||
|
strength=(.60 if finish=='holographic' else .56) if strength is None else strength
|
||||||
|
roughness=(.23 if finish=='holographic' else .19) if roughness is None else roughness
|
||||||
|
if not 0<=strength<=1 or not 0<=roughness<=1 or not 0<=linen_strength<=1:
|
||||||
|
raise ValueError('strength, roughness, linen_strength must be between zero and one')
|
||||||
|
mat=bpy.data.materials.get(name) or bpy.data.materials.new(name)
|
||||||
|
mat.use_nodes=True;t=mat.node_tree;t.nodes.clear()
|
||||||
|
uv=node(t,'ShaderNodeTexCoord','Artwork coordinates')
|
||||||
|
tex=node(t,'ShaderNodeTexImage','Front artwork');tex.image=artwork
|
||||||
|
t.links.new(uv.outputs['UV'],tex.inputs[0])
|
||||||
|
shader=node(t,'ShaderNodeGroup','Finish parameters');shader.node_tree=finish_group()
|
||||||
|
t.links.new(tex.outputs['Color'],shader.inputs['Artwork']);t.links.new(uv.outputs['UV'],shader.inputs['UV'])
|
||||||
|
shader.inputs['Finish Strength'].default_value=strength;shader.inputs['Finish Roughness'].default_value=roughness
|
||||||
|
shader.inputs['Holographic'].default_value=float(finish=='holographic')
|
||||||
|
shader.inputs['Anisotropy'].default_value=.45 if finish=='holographic' else .32
|
||||||
|
shader.inputs['Ink Roughness'].default_value=.77 if surface_texture=='linen' else (.61 if surface_texture=='paper' else .36)
|
||||||
|
shader.inputs['Sheen'].default_value=.16 if surface_texture=='linen' else 0
|
||||||
|
if finish_mask:
|
||||||
|
mask=node(t,'ShaderNodeTexImage','Optional finish mask');mask.image=finish_mask
|
||||||
|
t.links.new(uv.outputs['UV'],mask.inputs[0]);t.links.new(mask.outputs['Color'],shader.inputs['Coverage'])
|
||||||
|
elif mask_mode=='automatic':
|
||||||
|
mask=node(t,'ShaderNodeGroup','Automatic color mask');mask.node_tree=auto_mask_group()
|
||||||
|
t.links.new(tex.outputs['Color'],mask.inputs[0]);t.links.new(mask.outputs[0],shader.inputs['Coverage'])
|
||||||
|
if surface_texture!='smooth':
|
||||||
|
micro=node(t,'ShaderNodeGroup','Surface texture');micro.node_tree=bpy.data.node_groups['NG_CardMicroNormal']
|
||||||
|
micro.inputs['Linen Strength'].default_value=linen_strength if surface_texture=='linen' else 0
|
||||||
|
micro.inputs['Micro Texture Strength'].default_value=.045 if surface_texture=='linen' else .08
|
||||||
|
t.links.new(uv.outputs['Generated'],micro.inputs['Vector']);t.links.new(micro.outputs['Normal'],shader.inputs['Normal'])
|
||||||
|
else:
|
||||||
|
geo=node(t,'ShaderNodeNewGeometry','Smooth surface normal');t.links.new(geo.outputs['Normal'],shader.inputs['Normal'])
|
||||||
|
out=node(t,'ShaderNodeOutputMaterial','Card output');t.links.new(shader.outputs[0],out.inputs[0])
|
||||||
|
for index,n in enumerate(t.nodes):n.location=((index%4)*260,-(index//4)*300)
|
||||||
|
for k,v in dict(frontTexture=artwork.filepath,finish=finish,surfaceTexture=surface_texture,
|
||||||
|
maskMode='explicit' if finish_mask else mask_mode,finishStrength=strength,
|
||||||
|
finishRoughness=roughness,linenStrength=linen_strength if surface_texture=='linen' else 0).items():
|
||||||
|
mat['CardRenderConfig.'+k]=v
|
||||||
|
return mat
|
||||||
145
sanctification-tcg/blender_prototype/premium_finishes.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"""Non-destructive material-only revision; call apply_premium_finishes() in Blender.
|
||||||
|
|
||||||
|
UV protection regions are specific to legendary.png. Replace these regions when
|
||||||
|
using a different portrait; they are an editable prototype mask, not segmentation.
|
||||||
|
"""
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def node(tree, kind, name):
|
||||||
|
n = tree.nodes.new(kind)
|
||||||
|
n.name = n.label = name
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def math_node(t, op, a, b=0, name=None):
|
||||||
|
n = node(t, 'ShaderNodeMath', name or op)
|
||||||
|
n.operation = op
|
||||||
|
for i, value in enumerate((a, b)):
|
||||||
|
if isinstance(value, (float, int)):
|
||||||
|
n.inputs[i].default_value = value
|
||||||
|
else:
|
||||||
|
t.links.new(value, n.inputs[i])
|
||||||
|
return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def ramp(t, value, lo, hi, name):
|
||||||
|
n = node(t, 'ShaderNodeMapRange', name)
|
||||||
|
n.clamp = True
|
||||||
|
n.interpolation_type = 'SMOOTHSTEP'
|
||||||
|
n.inputs['From Min'].default_value = lo
|
||||||
|
n.inputs['From Max'].default_value = hi
|
||||||
|
t.links.new(value, n.inputs['Value'])
|
||||||
|
return n.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def ellipse(t, uv, cx, cy, rx, ry, name):
|
||||||
|
x = math_node(t, 'DIVIDE', math_node(t, 'SUBTRACT', uv.outputs[0], cx), rx)
|
||||||
|
y = math_node(t, 'DIVIDE', math_node(t, 'SUBTRACT', uv.outputs[1], cy), ry)
|
||||||
|
d = math_node(t, 'ADD', math_node(t, 'MULTIPLY', x, x), math_node(t, 'MULTIPLY', y, y))
|
||||||
|
return ramp(t, d, .65, 1.2, name)
|
||||||
|
|
||||||
|
|
||||||
|
def make_mask():
|
||||||
|
name = 'NG_David_PrintProtection'
|
||||||
|
if name in bpy.data.node_groups:
|
||||||
|
return bpy.data.node_groups[name]
|
||||||
|
g = bpy.data.node_groups.new(name, 'ShaderNodeTree')
|
||||||
|
for nm, st in [('Artwork', 'NodeSocketColor'), ('UV', 'NodeSocketVector')]:
|
||||||
|
g.interface.new_socket(name=nm, in_out='INPUT', socket_type=st)
|
||||||
|
g.interface.new_socket(name='Finish Mask', in_out='OUTPUT', socket_type='NodeSocketFloat')
|
||||||
|
inp = node(g, 'NodeGroupInput', 'Source artwork and UV')
|
||||||
|
out = node(g, 'NodeGroupOutput', 'Protected decorative finish mask')
|
||||||
|
uv = node(g, 'ShaderNodeSeparateXYZ', 'Artwork coordinates')
|
||||||
|
g.links.new(inp.outputs['UV'], uv.inputs[0])
|
||||||
|
hsv = node(g, 'ShaderNodeSeparateColor', 'Saturation selection')
|
||||||
|
hsv.mode = 'HSV'
|
||||||
|
g.links.new(inp.outputs['Artwork'], hsv.inputs[0])
|
||||||
|
mask = ramp(g, hsv.outputs[1], .18, .62, 'Saturated glass and gold')
|
||||||
|
mask = math_node(g, 'MULTIPLY', mask, ramp(g, hsv.outputs[2], .015, .16, 'Protect dark ink'))
|
||||||
|
for cx, cy, rx, ry, label in [(.50,.72,.19,.23,'Face and neck'),
|
||||||
|
(.64,.44,.17,.13,'Hand'),
|
||||||
|
(.29,.44,.18,.18,'Lamb')]:
|
||||||
|
mask = math_node(g, 'MULTIPLY', mask, ellipse(g, uv,cx,cy,rx,ry,label))
|
||||||
|
# Soft central rectangle covering cream title/rules panel; ornament remains.
|
||||||
|
x = math_node(g,'ABSOLUTE',math_node(g,'SUBTRACT',uv.outputs[0],.5))
|
||||||
|
side = ramp(g,x,.34,.41,'Keep side ornament')
|
||||||
|
above = ramp(g,uv.outputs[1],.245,.29,'Protect title panel')
|
||||||
|
below = math_node(g,'SUBTRACT',1,ramp(g,uv.outputs[1],.07,.105,'Keep bottom ornament'))
|
||||||
|
panel = math_node(g,'MAXIMUM',side,math_node(g,'MAXIMUM',above,below))
|
||||||
|
mask = math_node(g,'MULTIPLY',mask,panel)
|
||||||
|
g.links.new(mask,out.inputs[0])
|
||||||
|
for i,n in enumerate(g.nodes): n.location=((i%8)*185,-(i//8)*170)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def apply_premium_finishes():
|
||||||
|
mask_group = make_mask()
|
||||||
|
variants = [('MAT_David_Foil',False,False),('MAT_David_Holo',True,False),
|
||||||
|
('MAT_David_MetalHolo',True,True)]
|
||||||
|
for name,holo,metal in variants:
|
||||||
|
mat=bpy.data.materials[name]
|
||||||
|
t=mat.node_tree
|
||||||
|
image=next(n.image for n in t.nodes if n.type=='TEX_IMAGE')
|
||||||
|
t.nodes.clear()
|
||||||
|
uv=node(t,'ShaderNodeTexCoord','Card UV')
|
||||||
|
tex=node(t,'ShaderNodeTexImage','Printed David artwork');tex.image=image
|
||||||
|
t.links.new(uv.outputs['UV'],tex.inputs['Vector'])
|
||||||
|
mask=node(t,'ShaderNodeGroup','Protected finish regions');mask.node_tree=mask_group
|
||||||
|
t.links.new(tex.outputs['Color'],mask.inputs['Artwork']);t.links.new(uv.outputs['UV'],mask.inputs['UV'])
|
||||||
|
ink=node(t,'ShaderNodeBsdfPrincipled','Printed ink layer')
|
||||||
|
t.links.new(tex.outputs['Color'],ink.inputs['Base Color'])
|
||||||
|
ink.inputs['Roughness'].default_value=.36
|
||||||
|
ink.inputs['Coat Weight'].default_value=0
|
||||||
|
ink.inputs['Specular IOR Level'].default_value=.16
|
||||||
|
ink.inputs['Coat Roughness'].default_value=.24
|
||||||
|
finish=node(t,'ShaderNodeBsdfPrincipled','Diffractive reflection' if holo else 'Metal foil underprint')
|
||||||
|
finish.inputs['Metallic'].default_value=1
|
||||||
|
finish.inputs['Roughness'].default_value=.23 if holo else .19
|
||||||
|
finish.inputs['Anisotropic'].default_value=.45 if holo else .32
|
||||||
|
tangent=node(t,'ShaderNodeTangent','UV groove direction');tangent.direction_type='UV_MAP';tangent.uv_map='UV_Card_Artwork'
|
||||||
|
t.links.new(tangent.outputs[0],finish.inputs['Tangent'])
|
||||||
|
strength=node(t,'ShaderNodeValue','Finish Strength');strength.outputs[0].default_value=.72 if metal else (.60 if holo else .56)
|
||||||
|
weight=math_node(t,'MULTIPLY',mask.outputs[0],strength.outputs[0],'Masked coating coverage')
|
||||||
|
if holo:
|
||||||
|
geo=node(t,'ShaderNodeNewGeometry','View direction')
|
||||||
|
transform=node(t,'ShaderNodeVectorTransform','View in card coordinates')
|
||||||
|
transform.vector_type='VECTOR';transform.convert_from='WORLD';transform.convert_to='OBJECT'
|
||||||
|
t.links.new(geo.outputs['Incoming'],transform.inputs['Vector'])
|
||||||
|
v=node(t,'ShaderNodeSeparateXYZ','Signed view components');t.links.new(transform.outputs[0],v.inputs[0])
|
||||||
|
p=node(t,'ShaderNodeSeparateXYZ','Broad spatial phase');t.links.new(uv.outputs['UV'],p.inputs[0])
|
||||||
|
phase=math_node(t,'ADD',math_node(t,'MULTIPLY',v.outputs[0],2.8),math_node(t,'MULTIPLY',v.outputs[1],1.6))
|
||||||
|
phase=math_node(t,'ADD',phase,math_node(t,'MULTIPLY',p.outputs[0],.65))
|
||||||
|
phase=math_node(t,'ADD',phase,math_node(t,'MULTIPLY',p.outputs[1],.25))
|
||||||
|
phase=math_node(t,'FRACT',math_node(t,'ADD',phase,.3))
|
||||||
|
spectrum=node(t,'ShaderNodeValToRGB','Broad diffraction spectrum')
|
||||||
|
colors=[(0,(1,.32,.12,1)),(.2,(1,.84,.24,1)),(.4,(.12,1,.57,1)),(.6,(.12,.6,1,1)),(.8,(.68,.24,1,1)),(1,(1,.32,.12,1))]
|
||||||
|
for e in list(spectrum.color_ramp.elements)[1:-1]:spectrum.color_ramp.elements.remove(e)
|
||||||
|
spectrum.color_ramp.elements[0].color=colors[0][1];spectrum.color_ramp.elements[-1].color=colors[-1][1]
|
||||||
|
for pos,c in colors[1:-1]:spectrum.color_ramp.elements.new(pos).color=c
|
||||||
|
t.links.new(phase,spectrum.inputs[0]);t.links.new(spectrum.outputs[0],finish.inputs['Base Color'])
|
||||||
|
facing=node(t,'ShaderNodeLayerWeight','Grazing angle activation')
|
||||||
|
activation=ramp(t,facing.outputs['Facing'],.015,.38,'Tilt response')
|
||||||
|
activation=math_node(t,'ADD',.10,math_node(t,'MULTIPLY',activation,.90))
|
||||||
|
weight=math_node(t,'MULTIPLY',weight,activation,'Angle gated coating')
|
||||||
|
else:
|
||||||
|
t.links.new(tex.outputs['Color'],finish.inputs['Base Color'])
|
||||||
|
mix=node(t,'ShaderNodeMixShader','Printed ink plus reflective finish')
|
||||||
|
t.links.new(weight,mix.inputs[0]);t.links.new(ink.outputs[0],mix.inputs[1]);t.links.new(finish.outputs[0],mix.inputs[2])
|
||||||
|
out=node(t,'ShaderNodeOutputMaterial','Card surface');t.links.new(mix.outputs[0],out.inputs[0])
|
||||||
|
for i,n in enumerate(t.nodes):n.location=((i%7)*220,-(i//7)*240)
|
||||||
|
mat['Finish revision']='Reflective coating v3; protected print; no diffuse rainbow or thin-film outlines'
|
||||||
|
mat['CardRenderConfig.finishStrength']=strength.outputs[0].default_value
|
||||||
|
mat['CardRenderConfig.roughness']=finish.inputs['Roughness'].default_value
|
||||||
|
mat['CardRenderConfig.holo']=1.0 if holo else 0.0
|
||||||
|
mat['CardRenderConfig.holo_groove']=0.0
|
||||||
|
mat['CardRenderConfig.metallic']=1.0
|
||||||
|
# Low-power vertical strip intersects the existing demo's +12 degree tilt.
|
||||||
|
from mathutils import Vector
|
||||||
|
strip=bpy.data.objects['LIGHT_Specular_Strip']
|
||||||
|
strip.location=(.10,.012,.23)
|
||||||
|
strip.rotation_euler=(Vector((0,0,0))-strip.location).to_track_quat('-Z','Y').to_euler()
|
||||||
|
strip.data.energy=.65
|
||||||
|
strip.data.size=.018
|
||||||
|
strip.data.size_y=.14
|
||||||
|
return {'materials':[v[0] for v in variants],'mask':mask_group.name}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Review at matched poses. Does not alter saved scene state."""
|
||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
s=bpy.context.scene
|
||||||
|
out=Path('/home/dkzver/dev/sanctification-tcg/blender_prototype/renders')
|
||||||
|
objs=[o for o in s.objects if o.name.startswith(('CARD_','LABEL_'))]
|
||||||
|
saved={o.name:(o.location.copy(),o.rotation_euler.copy(),o.hide_render) for o in objs}
|
||||||
|
cam=bpy.data.objects['CAM_Front_Inspection']
|
||||||
|
cs=(cam.location.copy(),cam.rotation_euler.copy(),cam.data.ortho_scale)
|
||||||
|
settings=(s.camera,s.render.resolution_x,s.render.resolution_y,s.render.filepath)
|
||||||
|
def isolate(name,angle):
|
||||||
|
for o in objs:o.hide_render=o.name!=name
|
||||||
|
card=bpy.data.objects[name];card.location=(0,0,0);card.rotation_euler=(0,math.radians(angle),0)
|
||||||
|
s.camera=cam;s.render.resolution_x=750;s.render.resolution_y=1050
|
||||||
|
return card
|
||||||
|
def shot(filename):
|
||||||
|
s.render.filepath=str(out/filename);bpy.ops.render.render(write_still=True)
|
||||||
|
try:
|
||||||
|
isolate('CARD_Timothy_LinenFoil',0);shot('timothy_linen_foil_head_on.png')
|
||||||
|
isolate('CARD_Timothy_LinenFoil',12);shot('timothy_linen_foil_reflection.png')
|
||||||
|
for name in ['Timothy_Linen','Timothy_LinenFoil']:
|
||||||
|
isolate('CARD_'+name,12);cam.data.ortho_scale=.036;cam.location.y=-.018
|
||||||
|
shot(name.lower()+'_macro.png');cam.location=cs[0];cam.data.ortho_scale=cs[2]
|
||||||
|
for o in objs:o.hide_render=True
|
||||||
|
for name,x,label_text in [('Timothy_Linen',-.038,'Linen / Matte'),('Timothy_LinenFoil',.038,'Linen / Foil')]:
|
||||||
|
card=bpy.data.objects['CARD_'+name];card.hide_render=False;card.location=(x,0,0);card.rotation_euler=(0,math.radians(12),0)
|
||||||
|
label=bpy.data.objects['LABEL_'+name];label.hide_render=False;label.location=(x,-.050,.0015)
|
||||||
|
s.camera=cam;cam.data.ortho_scale=.16;s.render.resolution_x=1400;s.render.resolution_y=1050
|
||||||
|
shot('timothy_linen_vs_linen_foil.png')
|
||||||
|
if globals().get('RENDER_MOTION',False):
|
||||||
|
card=isolate('CARD_Timothy_LinenFoil',0)
|
||||||
|
s.render.resolution_x=540;s.render.resolution_y=720;cam.data.ortho_scale=.108
|
||||||
|
frames=out/'linen_foil_frames';frames.mkdir(exist_ok=True)
|
||||||
|
for f in range(72):
|
||||||
|
angle=-18+36*(.5-.5*math.cos(math.pi*f/71))
|
||||||
|
card.rotation_euler=(0,math.radians(angle),0)
|
||||||
|
shot('linen_foil_frames/linen_foil_%04d.png'%(f+1))
|
||||||
|
finally:
|
||||||
|
for o in objs:
|
||||||
|
loc,rot,h=saved[o.name];o.location=loc;o.rotation_euler=rot;o.hide_render=h
|
||||||
|
cam.location,cam.rotation_euler,cam.data.ortho_scale=cs
|
||||||
|
s.camera,s.render.resolution_x,s.render.resolution_y,s.render.filepath=settings
|
||||||
|
result={'directory':str(out),'restored_scene':True}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Run with Blender MCP. REVIEW_ANIMATION=True also regenerates demo frames."""
|
||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
scene=bpy.context.scene
|
||||||
|
out=Path('/home/dkzver/dev/sanctification-tcg/blender_prototype/renders')
|
||||||
|
cards=[o for o in scene.objects if o.name.startswith('CARD_')]
|
||||||
|
labels=[o for o in scene.objects if o.name.startswith('LABEL_')]
|
||||||
|
saved={o.name:(o.location.copy(),o.rotation_euler.copy(),o.hide_render) for o in cards+labels}
|
||||||
|
camera=scene.camera
|
||||||
|
resolution=(scene.render.resolution_x,scene.render.resolution_y)
|
||||||
|
frame=scene.frame_current
|
||||||
|
path=scene.render.filepath
|
||||||
|
def render(name,target=None,tilt=0):
|
||||||
|
for o in cards:o.hide_render=(o.name!=target if target else o.name in {'CARD_Holo_Demo','CARD_Wear_Test'})
|
||||||
|
for o in labels:o.hide_render=target is not None
|
||||||
|
scene.camera=bpy.data.objects['CAM_Front_Inspection' if target else 'CAM_Comparison']
|
||||||
|
scene.render.resolution_x=750 if target else 1280
|
||||||
|
scene.render.resolution_y=1050 if target else 900
|
||||||
|
if target:
|
||||||
|
o=bpy.data.objects[target];o.location=(0,0,0);o.rotation_euler=(0,math.radians(tilt),0)
|
||||||
|
scene.render.image_settings.file_format='PNG'
|
||||||
|
scene.render.filepath=str(out/name)
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
if target:
|
||||||
|
o.location=saved[target][0];o.rotation_euler=saved[target][1]
|
||||||
|
try:
|
||||||
|
render('comparison_lineup.png')
|
||||||
|
render('timothy_matte_closeup.png','CARD_Timothy_Matte')
|
||||||
|
render('timothy_linen_closeup.png','CARD_Timothy_Linen',-14)
|
||||||
|
render('timothy_gloss_closeup.png','CARD_Timothy_Gloss',8)
|
||||||
|
render('david_paper_closeup.png','CARD_David_Paper')
|
||||||
|
render('wear_test_timothy.png','CARD_Wear_Test')
|
||||||
|
render('universal_card_back.png','CARD_Timothy_Matte',180)
|
||||||
|
render('david_foil_closeup.png','CARD_David_Foil',0)
|
||||||
|
render('david_foil_reflection.png','CARD_David_Foil',12)
|
||||||
|
render('david_holographic_head_on.png','CARD_David_Holo',0)
|
||||||
|
render('david_holographic_spectrum_angle.png','CARD_David_Holo',12)
|
||||||
|
render('david_holographic_opposite_angle.png','CARD_David_Holo',-18)
|
||||||
|
render('card_three_quarter_thickness.png','CARD_David_Foil',55)
|
||||||
|
if globals().get('REVIEW_ANIMATION',False):
|
||||||
|
for o in cards:o.hide_render=o.name!='CARD_Holo_Demo'
|
||||||
|
for o in labels:o.hide_render=True
|
||||||
|
scene.camera=bpy.data.objects['CAM_Front_Inspection']
|
||||||
|
scene.render.resolution_x=540;scene.render.resolution_y=720
|
||||||
|
(out/'holo_frames').mkdir(exist_ok=True)
|
||||||
|
scene.render.filepath=str(out/'holo_frames'/'holo_')
|
||||||
|
bpy.ops.render.render(animation=True)
|
||||||
|
finally:
|
||||||
|
scene.frame_set(frame)
|
||||||
|
for o in cards+labels:
|
||||||
|
loc,rot,hidden=saved[o.name];o.location=loc;o.rotation_euler=rot;o.hide_render=hidden
|
||||||
|
scene.camera=camera
|
||||||
|
scene.render.resolution_x,scene.render.resolution_y=resolution
|
||||||
|
scene.render.filepath=path
|
||||||
|
result={'render_directory':str(out),'animation':globals().get('REVIEW_ANIMATION',False)}
|
||||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1011 KiB |
|
After Width: | Height: | Size: 1010 KiB |
|
After Width: | Height: | Size: 1010 KiB |
|
After Width: | Height: | Size: 1011 KiB |
|
After Width: | Height: | Size: 1011 KiB |
|
After Width: | Height: | Size: 1011 KiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1013 KiB |
|
After Width: | Height: | Size: 1013 KiB |
|
After Width: | Height: | Size: 1015 KiB |
|
After Width: | Height: | Size: 1016 KiB |
|
After Width: | Height: | Size: 1017 KiB |
|
After Width: | Height: | Size: 1018 KiB |
|
After Width: | Height: | Size: 1018 KiB |
|
After Width: | Height: | Size: 1020 KiB |
|
After Width: | Height: | Size: 1021 KiB |
|
After Width: | Height: | Size: 1022 KiB |
|
After Width: | Height: | Size: 1024 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |