🌀 Procedural Art Systems
Welcome to the frontier of digital art creation! In this lesson, we'll explore generative and procedural systems - brushes and workflows that create art semi-autonomously based on rules and algorithms. This is where you stop painting every leaf and start orchestrating systems that paint forests!
🎯 The Procedural Paradigm Shift
Traditional painting: You paint every mark manually.
Procedural painting: You define rules and systems that generate art based on parameters.
"The artist of the future doesn't just paint - they program creativity. A procedural system can generate infinite variations, explore design space automatically, and create complexity that would take days to paint manually."
⚠️ Prerequisites
This is an advanced lesson building on previous concepts. You should be comfortable with:
- ✅ Brush physics and mathematics (Lesson 1.1)
- ✅ Noise functions and pattern generation
- ✅ Algorithmic thinking and pseudo-code
- ✅ Custom brush creation from scratch
- ✅ Performance optimization concepts
🎯 Mastery Objectives
By the end of this comprehensive lesson, you will master:
- Generative Brush Patterns: Create brushes that generate complex patterns automatically
- Rule-Based Painting Systems: Define rules that control how art is created
- Automated Detail Generation: Let algorithms add detail intelligently
- Pattern-Based Workflows: Work with repeating elements efficiently
- Fractal and Noise Brushes: Harness mathematical patterns for organic results
- L-Systems and Recursive Art: Create trees, plants, and natural patterns algorithmically
- Portfolio Project: Build a procedural landscape generator
Generative Brush Patterns 🎲
Generative brushes don't just paint what you tell them - they create patterns based on rules. Think of them as artistic algorithms that generate unique results every time, while still following your creative direction.
🌟 The Power of Generation: One brush stroke that generates a tree branch with leaves, bark texture, and natural variation. One stroke that creates a crowd of people. One stroke that paints an entire flower field. That's the power of procedural generation!
Understanding Generative Systems
Types of Generative Patterns
| Pattern Type | Generation Method | Best For | Complexity |
|---|---|---|---|
| Recursive | Function calls itself with modified parameters | Trees, branches, fractals | Medium-High |
| Particle Systems | Many small elements following rules | Fire, smoke, crowds, swarms | Medium |
| Cellular Automata | Grid cells evolve based on neighbors | Organic growth, textures | Low-Medium |
| Voronoi Diagrams | Space divided by nearest point | Cellular structures, cracks | Medium |
| Flow Fields | Elements follow vector field | Hair, grass, water flow | Medium-High |
| L-Systems | String rewriting rules | Plants, organic patterns | High |
Building Your First Generative Brush
🌱 Example: Foliage Scatter Brush
CLASS FoliageGeneratorBrush:
// Parameters that control generation
leafDensity = 0.7 // How many leaves (0-1)
sizeVariation = 0.3 // How much size varies
colorVariation = 20 // Hue variation in degrees
clusteriness = 0.5 // How much leaves cluster
FUNCTION GenerateFoliage(position, radius, pressure):
// Step 1: Calculate number of leaves
leafCount = floor(leafDensity * 100 * pressure * (radius / 50))
// Step 2: Generate leaf positions
leafPositions = []
IF clusteriness > 0.5:
// Clustered distribution
clusterCount = max(3, leafCount / 10)
clusters = GenerateClusterCenters(clusterCount, radius)
FOR i FROM 0 TO leafCount:
cluster = ChooseRandomCluster(clusters)
offset = RandomGaussian() * radius * 0.3
leafPos = cluster + offset
leafPositions.push(leafPos)
END FOR
ELSE:
// Uniform distribution
FOR i FROM 0 TO leafCount:
angle = Random(0, TWO_PI)
distance = sqrt(Random()) * radius
leafPos = position + PolarToCartesian(distance, angle)
leafPositions.push(leafPos)
END FOR
END IF
// Step 3: Generate and draw each leaf
FOR EACH leafPos IN leafPositions:
// Vary size
leafSize = radius * 0.1 * (1 + Random(-sizeVariation, sizeVariation))
// Vary color
baseHue = 120 // Green
leafHue = baseHue + Random(-colorVariation, colorVariation)
leafSat = 60 + Random(-10, 10)
leafBright = 40 + Random(-15, 15)
leafColor = HSL(leafHue, leafSat, leafBright)
// Vary rotation
leafRotation = Random(0, TWO_PI)
// Vary shape (use different leaf shapes)
leafShape = ChooseRandom(['oval', 'pointed', 'lobed'])
// Draw leaf
DrawLeaf(leafPos, leafSize, leafRotation, leafColor, leafShape)
// Optional: Add stem
IF Random() < 0.3:
DrawStem(leafPos, leafSize * 0.5, leafRotation)
END IF
END FOR
// Step 4: Add depth cues
// Leaves farther from center are darker/smaller (depth)
FOR EACH leafPos IN leafPositions:
distFromCenter = Distance(leafPos, position)
depthFactor = distFromCenter / radius
// Darken distant leaves
ApplyDepthShading(leafPos, depthFactor * 0.3)
END FOR
END FUNCTION
FUNCTION DrawLeaf(position, size, rotation, color, shape):
SWITCH shape:
CASE 'oval':
DrawEllipse(position, size, size * 1.5, rotation, color)
CASE 'pointed':
DrawTriangle(position, size, size * 2, rotation, color)
CASE 'lobed':
DrawLobedLeaf(position, size, rotation, color)
END SWITCH
// Add vein detail
IF size > 5: // Only on larger leaves
veinColor = Darken(color, 0.2)
DrawVein(position, size, rotation, veinColor)
END IF
END FUNCTION
FUNCTION DrawVein(position, size, rotation, color):
// Central vein
startPos = position - Vector(0, size * 0.7).Rotate(rotation)
endPos = position + Vector(0, size * 0.7).Rotate(rotation)
DrawLine(startPos, endPos, 1, color)
// Side veins
FOR i FROM -3 TO 3:
IF i == 0: CONTINUE
offset = i * size * 0.3
veinStart = position + Vector(0, offset).Rotate(rotation)
veinEnd = veinStart + Vector(size * 0.3, 0).Rotate(rotation + PI/6)
DrawLine(veinStart, veinEnd, 0.5, color)
END FOR
END FUNCTION
END CLASS
💡 Key Insight: The power of generative brushes isn't just speed - it's controlled variation. Every leaf is unique, but they all follow your artistic direction. It's like having an assistant who understands your style and fills in details consistently!
Advanced Generative Techniques
Technique 1: Weighted Randomness
Not all random outcomes should be equally likely. Use weighted randomness for more natural results:
FUNCTION WeightedRandom(choices):
// choices = [{value: "small", weight: 0.6},
// {value: "medium", weight: 0.3},
// {value: "large", weight: 0.1}]
totalWeight = SUM(choice.weight FOR choice IN choices)
random = Random(0, totalWeight)
cumulative = 0
FOR EACH choice IN choices:
cumulative += choice.weight
IF random <= cumulative:
RETURN choice.value
END IF
END FOR
END FUNCTION
// Usage: Most leaves small, some medium, few large
leafSize = WeightedRandom([
{value: 5, weight: 0.6}, // 60% small
{value: 10, weight: 0.3}, // 30% medium
{value: 15, weight: 0.1} // 10% large
])
Technique 2: Coherent Noise for Natural Variation
Use Perlin/Simplex noise instead of pure randomness for smoother, more natural variation:
// BAD: Pure random - chaotic, unnatural
FOR EACH leaf:
leafSize = Random(5, 15) // Jumpy, no correlation
END FOR
// GOOD: Noise-based - smooth transitions
FOR EACH leaf AT position:
noiseValue = PerlinNoise(position.x * 0.01, position.y * 0.01)
leafSize = MapRange(noiseValue, -1, 1, 5, 15)
// Nearby leaves have similar sizes - more natural!
END FOR
Technique 3: Constraint-Based Generation
Add constraints to ensure aesthetically pleasing results:
FUNCTION GenerateWithConstraints(position, count):
generated = []
attempts = 0
WHILE generated.length < count AND attempts < count * 10:
candidate = GenerateCandidate(position)
// Check constraints
IF MeetsAestheticRules(candidate, generated):
generated.push(candidate)
END IF
attempts++
END WHILE
RETURN generated
END FUNCTION
FUNCTION MeetsAestheticRules(candidate, existing):
// Rule 1: Not too close to existing elements
FOR EACH element IN existing:
IF Distance(candidate, element) < minDistance:
RETURN FALSE
END IF
END FOR
// Rule 2: Within acceptable density
density = CalculateLocalDensity(candidate, existing)
IF density > maxDensity OR density < minDensity:
RETURN FALSE
END IF
// Rule 3: Respects composition (thirds, balance)
IF NOT InCompositionZone(candidate):
RETURN FALSE
END IF
RETURN TRUE
END FUNCTION
Rule-Based Painting Systems 📐
Rule-based systems take procedural art to the next level. Instead of just generating patterns, you create intelligent systems that make decisions based on context, follow artistic principles, and adapt to what they're painting.
🎯 The Rules Paradigm: "If painting sky, use cool colors. If near horizon, add atmospheric perspective. If painting tree, branches get thinner as they divide." Rules encode artistic knowledge into algorithms!
Understanding Rule Systems
Rule System Architecture
🏗️ Complete Rule-Based Painting Engine
CLASS RuleBasedPaintingSystem:
rules = []
context = {}
history = []
FUNCTION Initialize():
// Load rule sets
rules = [
ColorHarmonyRules(),
CompositionRules(),
DepthRules(),
DetailRules(),
StyleRules()
]
// Initialize context
context = {
canvasSize: {width, height},
viewportLocation: "landscape", // or "portrait", "abstract"
lightDirection: Vector3(-1, -1, 0),
timeOfDay: "noon", // affects color temperature
weatherCondition: "clear",
currentLayer: "base"
}
END FUNCTION
FUNCTION Paint(position, brushType, userData):
// Step 1: Gather context for this stroke
localContext = AnalyzeLocalContext(position)
// Step 2: Find applicable rules
applicableRules = []
FOR EACH rule IN rules:
IF rule.AppliesTo(localContext, context):
applicableRules.push(rule)
END IF
END FOR
// Step 3: Execute rules to modify painting parameters
paintParams = {
color: userData.color,
size: userData.size,
opacity: userData.opacity,
texture: userData.texture
}
FOR EACH rule IN applicableRules:
paintParams = rule.Modify(paintParams, localContext, context)
END FOR
// Step 4: Apply the stroke
ApplyStroke(position, paintParams)
// Step 5: Update history for future rules
history.push({
position: position,
params: paintParams,
timestamp: Now()
})
END FUNCTION
FUNCTION AnalyzeLocalContext(position):
return {
// Spatial analysis
distanceFromEdge: CalculateEdgeDistance(position),
verticalPosition: position.y / canvasHeight,
horizontalPosition: position.x / canvasWidth,
// Content analysis
existingColors: SampleNearbyColors(position, radius: 50),
existingDensity: CalculatePaintDensity(position),
dominantDirection: AnalyzeStrokeDirection(position),
// Compositional analysis
isOnThirdsLine: IsOnRuleOfThirds(position),
isInGoldenRatio: IsInGoldenRatioZone(position),
isFocalArea: IsNearFocalPoint(position),
// Recent activity
recentStrokes: GetRecentStrokesNear(position),
strokeDensity: CalculateRecentDensity(position)
}
END FUNCTION
END CLASS
Example Rule Sets
Rule Set 1: Atmospheric Perspective
CLASS AtmosphericPerspectiveRules:
FUNCTION AppliesTo(localContext, globalContext):
// Only apply to landscape scenes
RETURN globalContext.viewportLocation == "landscape"
END FUNCTION
FUNCTION Modify(paintParams, localContext, globalContext):
// Higher on canvas = farther away
distance = 1.0 - localContext.verticalPosition
// Rule 1: Distant objects are lighter
IF distance > 0.5:
lighteningFactor = (distance - 0.5) * 0.6
paintParams.color = Lighten(paintParams.color, lighteningFactor)
END IF
// Rule 2: Distant objects are less saturated
desaturationAmount = distance * 0.4
paintParams.color = Desaturate(paintParams.color, desaturationAmount)
// Rule 3: Distant objects are cooler (bluer)
IF distance > 0.6:
blueShift = (distance - 0.6) * 30 // degrees
paintParams.color = ShiftHue(paintParams.color,
towardBlue: blueShift)
END IF
// Rule 4: Distant objects have softer edges
IF distance > 0.7:
paintParams.hardness *= (1.0 - distance * 0.5)
END IF
// Rule 5: Distant objects have less detail
IF distance > 0.5:
paintParams.detailLevel *= (1.0 - distance * 0.7)
END IF
RETURN paintParams
END FUNCTION
END CLASS
Rule Set 2: Color Harmony
CLASS ColorHarmonyRules:
dominantHue = null
colorScheme = "analogous" // or "complementary", "triadic"
FUNCTION AppliesTo(localContext, globalContext):
// Always applicable
RETURN true
END FUNCTION
FUNCTION Modify(paintParams, localContext, globalContext):
// Analyze dominant hue if not set
IF dominantHue == null:
dominantHue = AnalyzeDominantHue(localContext.existingColors)
END IF
currentHue = GetHue(paintParams.color)
SWITCH colorScheme:
CASE "analogous":
// Keep colors within 30 degrees
deviation = abs(currentHue - dominantHue)
IF deviation > 30:
// Pull toward dominant hue
targetHue = dominantHue +
(currentHue > dominantHue ? 25 : -25)
paintParams.color = SetHue(paintParams.color, targetHue)
END IF
CASE "complementary":
// Allow dominant and opposite hues
oppositeHue = (dominantHue + 180) % 360
distToDominant = min(
abs(currentHue - dominantHue),
360 - abs(currentHue - dominantHue)
)
distToOpposite = min(
abs(currentHue - oppositeHue),
360 - abs(currentHue - oppositeHue)
)
// If not near either, push to closer one
IF distToDominant > 30 AND distToOpposite > 30:
IF distToDominant < distToOpposite:
paintParams.color = SetHue(paintParams.color,
dominantHue)
ELSE:
paintParams.color = SetHue(paintParams.color,
oppositeHue)
END IF
END IF
CASE "triadic":
// Allow three evenly-spaced hues
triad = [
dominantHue,
(dominantHue + 120) % 360,
(dominantHue + 240) % 360
]
// Find closest triad color
closest = triad[0]
minDist = 360
FOR EACH triadHue IN triad:
dist = min(
abs(currentHue - triadHue),
360 - abs(currentHue - triadHue)
)
IF dist < minDist:
minDist = dist
closest = triadHue
END IF
END FOR
// Pull toward closest if too far
IF minDist > 25:
paintParams.color = SetHue(paintParams.color, closest)
END IF
END SWITCH
RETURN paintParams
END FUNCTION
END CLASS
Rule Set 3: Compositional Emphasis
CLASS CompositionRules:
focalPoints = []
FUNCTION Initialize():
// Define focal points (rule of thirds intersections)
focalPoints = [
{x: 0.33, y: 0.33, strength: 1.0},
{x: 0.67, y: 0.33, strength: 1.0},
{x: 0.33, y: 0.67, strength: 0.8},
{x: 0.67, y: 0.67, strength: 0.8}
]
END FUNCTION
FUNCTION Modify(paintParams, localContext, globalContext):
// Calculate distance to nearest focal point
nearestFocal = FindNearestFocalPoint(localContext.position)
distToFocal = Distance(localContext.position, nearestFocal)
// Normalize distance (0 = at focal point, 1 = far away)
normalizedDist = min(1.0, distToFocal / (canvasWidth * 0.3))
// Rule 1: More detail near focal points
IF normalizedDist < 0.3:
detailBoost = (1.0 - normalizedDist / 0.3) * 0.5
paintParams.detailLevel *= (1.0 + detailBoost)
END IF
// Rule 2: Higher contrast near focal points
IF normalizedDist < 0.4:
contrastBoost = (1.0 - normalizedDist / 0.4) * 0.3
paintParams.contrast *= (1.0 + contrastBoost)
END IF
// Rule 3: More saturated colors near focal points
IF normalizedDist < 0.35:
saturationBoost = (1.0 - normalizedDist / 0.35) * 0.25
currentSat = GetSaturation(paintParams.color)
newSat = min(1.0, currentSat * (1.0 + saturationBoost))
paintParams.color = SetSaturation(paintParams.color, newSat)
END IF
// Rule 4: Softer edges away from focal points
IF normalizedDist > 0.6:
softening = (normalizedDist - 0.6) * 0.5
paintParams.hardness *= (1.0 - softening)
END IF
RETURN paintParams
END FUNCTION
END CLASS
🎓 Advanced Concept: Rule-based systems can be trained on your art style! Analyze your finished work to extract rules: "I always use warm shadows," "I saturate focal points by 20%," "I use complementary colors for contrast." Code these observations into rules!
Smart Decision Making
Implementing Fuzzy Logic for Artistic Decisions
// Instead of binary rules (yes/no), use fuzzy logic (degrees)
CLASS FuzzyRule:
FUNCTION Evaluate(input):
// Returns 0-1 indicating rule strength
// Example: "If near horizon, apply atmospheric perspective"
distanceFromHorizon = abs(input.y - horizonY)
IF distanceFromHorizon < 50:
// Very close to horizon = strong rule application
RETURN 1.0
ELSE IF distanceFromHorizon < 200:
// Somewhat close = partial application
RETURN 1.0 - ((distanceFromHorizon - 50) / 150)
ELSE:
// Far from horizon = no application
RETURN 0.0
END IF
END FUNCTION
FUNCTION Apply(paintParams, strength):
// Apply rule proportionally to strength
// Instead of:
// IF condition: params.opacity = 0.5
// Do:
params.opacity = Lerp(params.opacity, targetOpacity, strength)
RETURN params
END FUNCTION
END CLASS
// Combine multiple fuzzy rules
FUNCTION CombineFuzzyRules(rules, input, params):
FOR EACH rule IN rules:
strength = rule.Evaluate(input)
IF strength > 0:
params = rule.Apply(params, strength)
END IF
END FOR
RETURN params
END FUNCTION
Context-Aware Brushes
🎨 Intelligent Brush Example: Smart Sky Painter
CLASS SmartSkyBrush:
FUNCTION Paint(position, pressure):
// Analyze what we're painting over
underlyingColor = SampleCanvas(position)
verticalPosition = position.y / canvasHeight
// Rule 1: Determine sky zone
IF verticalPosition < 0.3:
// Upper sky - deeper blue
baseColor = RGB(100, 149, 237) // Cornflower blue
cloudProbability = 0.3
ELSE IF verticalPosition < 0.6:
// Middle sky - medium blue
baseColor = RGB(135, 206, 235) // Sky blue
cloudProbability = 0.5
ELSE:
// Near horizon - lighter, warmer
baseColor = RGB(176, 224, 230) // Powder blue
// Add warmth near horizon
baseColor = ShiftHue(baseColor, towardWarm: 15)
cloudProbability = 0.2
END IF
// Rule 2: Add atmospheric variation
noiseValue = PerlinNoise(position.x * 0.01, position.y * 0.01)
colorVariation = noiseValue * 20 // Hue variation
finalColor = ShiftHue(baseColor, colorVariation)
// Rule 3: Maybe add cloud
IF Random() < cloudProbability * pressure:
cloudColor = RGB(255, 255, 255)
cloudOpacity = 0.6 + Random() * 0.3
// Clouds are softer and larger
DrawCloudPuff(position,
size: 40 + Random() * 30,
color: cloudColor,
opacity: cloudOpacity)
ELSE:
// Paint sky
DrawSkyWash(position,
size: 30 + pressure * 20,
color: finalColor,
opacity: 0.3 + pressure * 0.4)
END IF
// Rule 4: Blend with existing content
IF underlyingColor != backgroundColor:
// Something already painted here - blend gently
BlendWithExisting(position, softness: 0.7)
END IF
END FUNCTION
END CLASS
Automated Detail Generation 🔍
Why spend hours painting every brick in a wall, every leaf in a forest, or every blade of grass in a field? Automated detail generation analyzes your base painting and intelligently adds appropriate detail based on context!
Detail Generation Strategy
Detail Generation Strategies
| Strategy | When to Use | How It Works | Best For |
|---|---|---|---|
| Edge Detection | After base shapes defined | Find edges, add detail along them | Outlines, highlights, definition |
| Color Analysis | When surface type known | Analyze color to determine material | Material-specific details |
| Density Mapping | For scattered elements | Use density map to place details | Foliage, crowds, particles |
| Pattern Recognition | When patterns present | Detect pattern, extend it | Bricks, tiles, repetitive elements |
| Depth-Based | With depth information | More detail in foreground | Landscapes, scenes with depth |
Automated Detail Algorithms
🔍 Intelligent Detail Generator
CLASS AutomatedDetailGenerator:
FUNCTION GenerateDetails(canvas, detailLevel):
// Step 1: Analyze canvas content
analysis = AnalyzeCanvas(canvas)
// Step 2: Generate detail for each region
FOR EACH region IN analysis.regions:
GenerateRegionDetails(region, detailLevel)
END FOR
END FUNCTION
FUNCTION AnalyzeCanvas(canvas):
regions = []
// Segment canvas into regions by color similarity
segments = ColorSegmentation(canvas)
FOR EACH segment IN segments:
region = {
bounds: segment.boundingBox,
averageColor: segment.meanColor,
area: segment.pixelCount,
edges: DetectEdges(segment),
texture: AnalyzeTexture(segment),
materialType: ClassifyMaterial(segment),
depthEstimate: EstimateDepth(segment)
}
regions.push(region)
END FOR
RETURN {regions: regions}
END FUNCTION
FUNCTION GenerateRegionDetails(region, detailLevel):
// Determine detail type based on material
SWITCH region.materialType:
CASE "foliage":
GenerateFoliageDetails(region, detailLevel)
CASE "wood":
GenerateWoodGrain(region, detailLevel)
CASE "stone":
GenerateStoneTexture(region, detailLevel)
CASE "water":
GenerateWaterRipples(region, detailLevel)
CASE "sky":
GenerateCloudDetails(region, detailLevel)
CASE "fabric":
GenerateFabricWeave(region, detailLevel)
DEFAULT:
GenerateGenericDetails(region, detailLevel)
END SWITCH
// Always add edge details
GenerateEdgeDetails(region.edges, detailLevel)
END FUNCTION
FUNCTION GenerateFoliageDetails(region, detailLevel):
// Calculate detail density based on depth
density = detailLevel * (1.0 - region.depthEstimate)
leafCount = region.area * density * 0.001
// Extract base color
baseHue = GetHue(region.averageColor)
FOR i FROM 0 TO leafCount:
// Random position within region
position = RandomPointInRegion(region.bounds)
// Vary size (smaller = more distant)
depthAtPoint = EstimateLocalDepth(position, region)
leafSize = (3 + Random() * 4) * (1.0 - depthAtPoint * 0.7)
// Vary color slightly
leafHue = baseHue + Random(-15, 15)
leafSat = 0.6 + Random() * 0.3
leafBright = 0.4 + Random() * 0.3
leafColor = HSL(leafHue, leafSat, leafBright)
// Draw leaf
leafRotation = Random() * TWO_PI
DrawLeaf(position, leafSize, leafRotation, leafColor)
END FOR
END FUNCTION
FUNCTION GenerateWoodGrain(region, detailLevel):
// Wood has directional grain patterns
grainDirection = EstimateGrainDirection(region)
lineCount = region.area * detailLevel * 0.0005
FOR i FROM 0 TO lineCount:
// Start position
startPos = RandomPointInRegion(region.bounds)
// Follow grain direction with noise
lineLength = 20 + Random() * 30
points = []
currentPos = startPos
FOR step FROM 0 TO lineLength:
// Add point
points.push(currentPos)
// Move along grain with slight waviness
noise = PerlinNoise(currentPos.x * 0.1, currentPos.y * 0.1)
direction = grainDirection + noise * 0.3
currentPos = currentPos + PolarToCartesian(1, direction)
// Stop if out of region
IF NOT IsInRegion(currentPos, region.bounds):
BREAK
END IF
END FOR
// Draw grain line
grainColor = Darken(region.averageColor, 0.15)
DrawCurve(points, width: 0.5, color: grainColor, opacity: 0.3)
END FOR
END FUNCTION
FUNCTION GenerateEdgeDetails(edges, detailLevel):
// Add highlights and shadows along edges
FOR EACH edge IN edges:
// Determine edge type (convex or concave)
edgeType = ClassifyEdge(edge)
IF edgeType == "convex":
// Add highlight on light-facing side
highlightColor = RGB(255, 255, 255)
DrawAlongEdge(edge, highlightColor,
width: 1 + detailLevel,
opacity: 0.3 * detailLevel)
ELSE IF edgeType == "concave":
// Add shadow in crevice
shadowColor = RGB(0, 0, 0)
DrawAlongEdge(edge, shadowColor,
width: 1 + detailLevel,
opacity: 0.2 * detailLevel)
END IF
END FOR
END FUNCTION
FUNCTION ClassifyMaterial(segment):
// Use color and texture to guess material type
hue = GetHue(segment.meanColor)
saturation = GetSaturation(segment.meanColor)
brightness = GetBrightness(segment.meanColor)
textureComplexity = segment.texture.complexity
// Foliage: green, medium saturation
IF hue >= 90 AND hue <= 150 AND saturation > 0.3:
RETURN "foliage"
END IF
// Wood: brown/orange, low saturation
IF hue >= 20 AND hue <= 40 AND saturation < 0.5:
IF textureComplexity > 0.3:
RETURN "wood"
END IF
END IF
// Stone: gray, very low saturation
IF saturation < 0.2 AND brightness > 0.3 AND brightness < 0.7:
RETURN "stone"
END IF
// Water: blue-green, high saturation
IF hue >= 170 AND hue <= 210 AND saturation > 0.4:
RETURN "water"
END IF
// Sky: light blue, high brightness
IF hue >= 190 AND hue <= 220 AND brightness > 0.7:
RETURN "sky"
END IF
RETURN "generic"
END FUNCTION
END CLASS
💡 Pro Workflow: Paint your composition in broad strokes, establish values and colors, then run automated detail generation as a final pass. You maintain creative control while the algorithm handles tedious repetitive work!
Intelligent Detail Placement
Smart Placement Algorithm
// Avoid uniform distribution - make it look natural
FUNCTION IntelligentPlacement(region, elementCount):
placements = []
// Create importance map
importanceMap = GenerateImportanceMap(region)
// Poisson disk sampling with importance weighting
candidates = []
attempts = 0
maxAttempts = elementCount * 10
WHILE placements.length < elementCount AND attempts < maxAttempts:
// Sample position weighted by importance
candidate = SampleByImportance(region, importanceMap)
// Check minimum distance to existing placements
minDist = CalculateMinDistance(candidate, region.depthEstimate)
IF IsValidPlacement(candidate, placements, minDist):
placements.push(candidate)
END IF
attempts++
END WHILE
RETURN placements
END FUNCTION
FUNCTION GenerateImportanceMap(region):
// Higher importance = more details
importanceMap = CreateMap(region.bounds)
// More important near edges
FOR EACH pixel IN region.bounds:
distToEdge = DistanceToNearestEdge(pixel, region.edges)
edgeImportance = exp(-distToEdge / 20) // Falloff
// More important in focal areas
distToFocus = DistanceToFocalPoint(pixel)
focalImportance = 1.0 - (distToFocus / maxDistance)
// More important in foreground
depth = EstimateDepth(pixel)
depthImportance = 1.0 - depth
// Combine factors
importance = edgeImportance * 0.3 +
focalImportance * 0.4 +
depthImportance * 0.3
importanceMap[pixel] = importance
END FOR
RETURN importanceMap
END FUNCTION
Fractal & Noise Brushes 🌀
Nature is full of self-similar patterns - coastlines, mountains, trees, clouds. Fractals and noise functions let you harness these patterns to create infinitely detailed, organic-looking art with minimal effort!
🌟 The Fractal Principle: "A fractal is a pattern where each part, no matter how small, resembles the whole." This principle appears everywhere in nature - and in the best procedural brushes!
Understanding Fractal Brushes
Fractal & Noise Functions Comparison
| Function | Characteristics | Computation Cost | Best For |
|---|---|---|---|
| Perlin Noise | Smooth, continuous gradients | Medium | Natural terrain, clouds, organic forms |
| Simplex Noise | Like Perlin but faster, less artifacts | Low-Medium | Real-time applications, same uses as Perlin |
| Worley Noise | Cellular, organic cell patterns | High | Stone, scales, cracked surfaces, cells |
| FBM (Fractal Brownian) | Multiple octaves, highly detailed | High | Complex terrain, detailed clouds, rich textures |
| Turbulence | Absolute value of noise, chaotic | Medium | Marble, fire, energy, swirls |
| Domain Warping | Noise distorted by other noise | Very High | Complex organic patterns, surreal effects |
Implementing Fractal Brushes
🌀 Fractal Brownian Motion Brush
CLASS FBMBrush:
octaves = 4 // Number of noise layers
lacunarity = 2.0 // Frequency multiplier per octave
persistence = 0.5 // Amplitude multiplier per octave
FUNCTION Paint(position, pressure, size):
// Generate FBM value for this position
fbmValue = FractalBrownianMotion(
position.x,
position.y,
octaves,
lacunarity,
persistence
)
// Map FBM value to brush properties
// FBM typically ranges from -1 to 1
normalizedFBM = (fbmValue + 1.0) / 2.0 // Now 0 to 1
// Use FBM to vary size
actualSize = size * (0.5 + normalizedFBM * 0.5)
// Use FBM to vary opacity
actualOpacity = pressure * (0.3 + normalizedFBM * 0.7)
// Use FBM to vary color (for terrain/organic effects)
colorShift = normalizedFBM * 40 // Hue shift
shiftedColor = ShiftHue(baseColor, colorShift)
// Apply the stamp
DrawStamp(position, actualSize, actualOpacity, shiftedColor)
END FUNCTION
FUNCTION FractalBrownianMotion(x, y, octaves, lacunarity, persistence):
value = 0.0
amplitude = 1.0
frequency = 1.0
maxValue = 0.0 // For normalization
FOR i FROM 0 TO octaves - 1:
// Get noise value at this frequency
noiseValue = PerlinNoise(x * frequency, y * frequency)
// Accumulate
value += noiseValue * amplitude
maxValue += amplitude
// Adjust for next octave
amplitude *= persistence
frequency *= lacunarity
END FOR
// Normalize to -1 to 1 range
RETURN value / maxValue
END FUNCTION
FUNCTION PerlinNoise(x, y):
// Get integer parts
xi = floor(x)
yi = floor(y)
// Get fractional parts
xf = x - xi
yf = y - yi
// Get gradients at corners
g00 = GradientAt(xi, yi)
g10 = GradientAt(xi + 1, yi)
g01 = GradientAt(xi, yi + 1)
g11 = GradientAt(xi + 1, yi + 1)
// Calculate dot products
d00 = Dot(g00, xf, yf)
d10 = Dot(g10, xf - 1, yf)
d01 = Dot(g01, xf, yf - 1)
d11 = Dot(g11, xf - 1, yf - 1)
// Interpolate
u = Fade(xf)
v = Fade(yf)
x1 = Lerp(d00, d10, u)
x2 = Lerp(d01, d11, u)
RETURN Lerp(x1, x2, v)
END FUNCTION
FUNCTION Fade(t):
// Smoothstep function: 6t^5 - 15t^4 + 10t^3
RETURN t * t * t * (t * (t * 6 - 15) + 10)
END FUNCTION
END CLASS
Advanced Fractal Techniques
Technique 1: Domain Warping
Use noise to distort the input coordinates of other noise - creates incredibly organic patterns!
FUNCTION DomainWarpedNoise(x, y):
// First layer of noise to warp coordinates
warpX = FBM(x * 0.5, y * 0.5, octaves: 2)
warpY = FBM(x * 0.5 + 100, y * 0.5, octaves: 2)
// Scale the warp
warpStrength = 40
warpedX = x + warpX * warpStrength
warpedY = y + warpY * warpStrength
// Sample noise at warped coordinates
finalNoise = FBM(warpedX * 0.02, warpedY * 0.02, octaves: 4)
RETURN finalNoise
END FUNCTION
// Creates flowing, organic patterns like wood grain or marble
Technique 2: Ridged Multifractal
Creates sharp ridges - perfect for mountain ranges and dramatic terrain!
FUNCTION RidgedMultifractal(x, y, octaves):
value = 0.0
amplitude = 1.0
frequency = 1.0
FOR i FROM 0 TO octaves - 1:
// Get noise
noise = PerlinNoise(x * frequency, y * frequency)
// Create ridge: 1 - |noise|
noise = 1.0 - abs(noise)
// Square for sharper ridges
noise = noise * noise
// Accumulate
value += noise * amplitude
// Next octave
amplitude *= 0.5
frequency *= 2.0
END FOR
RETURN value
END FUNCTION
Technique 3: Cellular/Worley Noise
FUNCTION WorleyNoise(x, y, pointDensity):
// Generate feature points in a grid
cellSize = 1.0 / pointDensity
// Find which cell we're in
cellX = floor(x / cellSize)
cellY = floor(y / cellSize)
minDist = Infinity
secondMinDist = Infinity
// Check this cell and neighbors
FOR offsetX FROM -1 TO 1:
FOR offsetY FROM -1 TO 1:
checkX = cellX + offsetX
checkY = cellY + offsetY
// Get random point in this cell
point = GetCellPoint(checkX, checkY, cellSize)
// Calculate distance
dist = Distance(x, y, point.x, point.y)
IF dist < minDist:
secondMinDist = minDist
minDist = dist
ELSE IF dist < secondMinDist:
secondMinDist = dist
END IF
END FOR
END FOR
// Return distance(s) - can use different combinations
RETURN {
f1: minDist, // Distance to nearest
f2: secondMinDist, // Distance to second nearest
f2_minus_f1: secondMinDist - minDist // Cell border
}
END FUNCTION
FUNCTION GetCellPoint(cellX, cellY, cellSize):
// Deterministic random point for this cell
seed = Hash(cellX, cellY)
random = PseudoRandom(seed)
RETURN {
x: (cellX + random.x) * cellSize,
y: (cellY + random.y) * cellSize
}
END FUNCTION
🎨 Artist's Application: Combine multiple noise functions for rich results! Use FBM for overall shape, Worley for cellular detail, turbulence for chaos, and domain warping for organic flow. Layer them like you layer paint!
Practical Fractal Brush Applications
Application 1: Mountain Terrain Brush
CLASS MountainTerrainBrush:
FUNCTION Paint(position, size):
FOR x FROM position.x - size TO position.x + size:
// Calculate height using ridged multifractal
height = RidgedMultifractal(x * 0.01, 0, octaves: 5)
// Scale height
actualHeight = height * size * 2
// Calculate color based on height
IF height < 0.3:
color = RGB(76, 175, 80) // Green (low)
ELSE IF height < 0.6:
color = RGB(139, 69, 19) // Brown (mid)
ELSE IF height < 0.8:
color = RGB(128, 128, 128) // Gray (high)
ELSE:
color = RGB(255, 255, 255) // White (peak/snow)
END IF
// Add noise to color
colorNoise = PerlinNoise(x * 0.05, height * 10)
color = VaryColor(color, colorNoise * 20)
// Draw vertical line for this x
DrawLine(
x, position.y,
x, position.y - actualHeight,
color
)
END FOR
END FUNCTION
END CLASS
Application 2: Organic Cloud Brush
CLASS OrganicCloudBrush:
FUNCTION Paint(position, size, pressure):
// Use FBM for cloud shape
FOR offsetX FROM -size TO size:
FOR offsetY FROM -size TO size:
px = position.x + offsetX
py = position.y + offsetY
// Calculate distance from center
dist = sqrt(offsetX * offsetX + offsetY * offsetY)
IF dist > size: CONTINUE
// Use FBM for density
density = FBM(px * 0.01, py * 0.01, octaves: 3)
// Fade out from center
falloff = 1.0 - (dist / size)
// Combine
opacity = density * falloff * pressure * 0.3
IF opacity > 0.05:
// Add color variation
brightness = 220 + density * 35
color = RGB(brightness, brightness, brightness)
BlendPixel(px, py, color, opacity)
END IF
END FOR
END FOR
END FUNCTION
END CLASS
L-Systems & Recursive Patterns 🌿
L-Systems (Lindenmayer Systems) are a mathematical formalism for describing the growth of plants and other organic structures. They're perfect for generating realistic trees, plants, and natural patterns!
🌱 The L-System Principle: Start with a simple axiom (seed), apply rewriting rules repeatedly, interpret the result as drawing commands. Simple rules create complex, natural-looking structures!
Understanding L-Systems
🌳 L-System Basics
// Simple L-System Example: Plant Growth
Axiom: F
Rules:
F → F[+F]F[-F]F
Interpretation:
F = Draw forward
+ = Turn right 25°
- = Turn left 25°
[ = Push position/angle to stack
] = Pop position/angle from stack
Generation:
0: F
1: F[+F]F[-F]F
2: F[+F]F[-F]F[+F[+F]F[-F]F]F[+F]F[-F]F[-F[+F]F[-F]F]F[+F]F[-F]F
(Continues exponentially...)
Result: Natural-looking branching plant!
L-System Implementation
Complete L-System Engine
CLASS LSystemGenerator:
axiom = ""
rules = {}
angle = 25
iterations = 0
FUNCTION Initialize(axiom, rules, angle, iterations):
this.axiom = axiom
this.rules = rules
this.angle = angle
this.iterations = iterations
END FUNCTION
FUNCTION Generate():
current = axiom
// Apply rules iteratively
FOR i FROM 0 TO iterations - 1:
current = ApplyRules(current)
END FOR
RETURN current
END FUNCTION
FUNCTION ApplyRules(input):
output = ""
FOR EACH character IN input:
IF rules.hasKey(character):
// Apply rule
output += rules[character]
ELSE:
// No rule, keep character
output += character
END IF
END FOR
RETURN output
END FUNCTION
FUNCTION Interpret(commands, startPosition, startAngle, stepSize):
position = startPosition
angle = startAngle
stack = [] // For storing state
FOR EACH command IN commands:
SWITCH command:
CASE 'F': // Draw forward
newPosition = position + PolarToCartesian(stepSize, angle)
DrawLine(position, newPosition)
position = newPosition
CASE 'f': // Move forward (no draw)
position += PolarToCartesian(stepSize, angle)
CASE '+': // Turn right
angle += this.angle * DEG_TO_RAD
CASE '-': // Turn left
angle -= this.angle * DEG_TO_RAD
CASE '[': // Push state
stack.push({position: position, angle: angle})
CASE ']': // Pop state
state = stack.pop()
position = state.position
angle = state.angle
CASE '|': // Turn around
angle += PI
END SWITCH
END FOR
END FUNCTION
END CLASS
L-System Plant Examples
Collection of Plant L-Systems
1. Simple Tree
Axiom: F
Rules: F → FF+[+F-F-F]-[-F+F+F]
Angle: 22.5°
Iterations: 4
Result: Realistic deciduous tree
2. Bush/Shrub
Axiom: F
Rules: F → F[+F]F[-F][F]
Angle: 20°
Iterations: 5
Result: Dense, bushy plant
3. Fern
Axiom: X
Rules:
X → F+[[X]-X]-F[-FX]+X
F → FF
Angle: 25°
Iterations: 6
Result: Realistic fern frond
4. Seaweed/Kelp
Axiom: F
Rules: F → F[+F]F[-F]+F
Angle: 15°
Iterations: 5
Result: Organic seaweed shape
5. Branching Coral
Axiom: F
Rules: F → FF-[-F+F]+[+F-F]
Angle: 25°
Iterations: 4
Result: Coral-like branching structure
Advanced L-System Techniques
🔬 Stochastic L-Systems
Add randomness for more natural variation!
// Instead of deterministic rules, use probability
Rules:
F → F[+F]F[-F]F (50%)
F → F[++F][-F]F (30%)
F → F[+F][--F]F (20%)
FUNCTION ApplyStochasticRules(character):
IF character == 'F':
random = Random(0, 1)
IF random < 0.5:
RETURN "F[+F]F[-F]F"
ELSE IF random < 0.8:
RETURN "F[++F][-F]F"
ELSE:
RETURN "F[+F][--F]F"
END IF
END IF
RETURN character
END FUNCTION
// Each generation creates unique variation!
🎨 Parametric L-Systems
Pass parameters through the system for fine control!
// Parameters can control length, thickness, etc.
Axiom: F(10,1)
Rules: F(l,w) → F(l*0.7,w*0.8)[+F(l*0.5,w*0.6)][-F(l*0.5,w*0.6)]
Interpretation:
F(l,w) = Draw forward length l, width w
Result: Branches taper naturally!
🌈 Context-Sensitive L-Systems
Rules depend on neighboring symbols!
// Syntax: left < symbol > right → replacement
Rules:
F < F > F → F // F between two Fs stays F
F < F > → F[+F] // F before nothing branches
< F > F → F[-F] // F after nothing branches
// Creates more realistic, context-aware growth!
Combining L-Systems with Art
Artistic L-System Brush
CLASS ArtisticLSystemBrush:
lsystem = LSystemGenerator()
FUNCTION Paint(position, size, style):
// Choose L-System based on style
SWITCH style:
CASE "tree":
lsystem.Initialize(
axiom: "F",
rules: {"F": "FF+[+F-F-F]-[-F+F+F]"},
angle: 22.5,
iterations: 4
)
CASE "fern":
lsystem.Initialize(
axiom: "X",
rules: {
"X": "F+[[X]-X]-F[-FX]+X",
"F": "FF"
},
angle: 25,
iterations: 5
)
CASE "bush":
lsystem.Initialize(
axiom: "F",
rules: {"F": "F[+F]F[-F][F]"},
angle: 20,
iterations: 4
)
END SWITCH
// Generate structure
commands = lsystem.Generate()
// Calculate step size based on desired size
// (More iterations = more detailed = smaller steps)
stepSize = size / pow(2, lsystem.iterations)
// Interpret with artistic flourishes
InterpretArtistically(commands, position, stepSize, style)
END FUNCTION
FUNCTION InterpretArtistically(commands, position, stepSize, style):
currentPos = position
currentAngle = -PI / 2 // Point up
stack = []
depth = 0 // Track depth for coloring
FOR EACH command IN commands:
SWITCH command:
CASE 'F':
newPos = currentPos + PolarToCartesian(stepSize, currentAngle)
// Artistic additions:
// 1. Color based on depth
color = CalculateDepthColor(depth, style)
// 2. Width based on depth (thicker at base)
width = max(1, 5 - depth * 0.5)
// 3. Add slight wobble for organic feel
wobble = PerlinNoise(currentPos.x * 0.1, currentPos.y * 0.1) * 2
adjustedAngle = currentAngle + wobble * 0.1
// 4. Draw with artistic style
DrawStylizedLine(currentPos, newPos, width, color, style)
// 5. Maybe add leaves at tips
IF depth > lsystem.iterations - 2:
IF Random() < 0.3:
DrawLeaf(newPos, adjustedAngle, style)
END IF
END IF
currentPos = newPos
CASE '+':
// Add slight randomness to angle
currentAngle += (lsystem.angle + Random(-3, 3)) * DEG_TO_RAD
CASE '-':
currentAngle -= (lsystem.angle + Random(-3, 3)) * DEG_TO_RAD
CASE '[':
stack.push({
position: currentPos,
angle: currentAngle,
depth: depth
})
depth++
CASE ']':
state = stack.pop()
currentPos = state.position
currentAngle = state.angle
depth = state.depth
END SWITCH
END FOR
END FUNCTION
FUNCTION CalculateDepthColor(depth, style):
IF style == "tree":
// Trunk = brown, branches = brown, leaves = green
IF depth < 2:
RETURN RGB(101, 67, 33) // Brown trunk
ELSE IF depth < 4:
RETURN RGB(139, 90, 43) // Lighter brown branches
ELSE:
RETURN RGB(76, 175, 80) // Green leaves
END IF
ELSE IF style == "fern":
// Gradient from dark to light green
greenShade = 100 + depth * 15
RETURN RGB(76, greenShade, 80)
END IF
END FUNCTION
END CLASS
💡 Pro Tip: L-Systems are perfect for instant forests! Generate multiple trees with slight parameter variations, place them procedurally, and you have a complete forest in seconds instead of hours!
Pattern-Based Workflows 🔄
Pattern-based workflows leverage repetition with variation to create complex artwork efficiently. Instead of painting every instance manually, you create smart systems that handle repetitive elements while maintaining artistic variation.
Pattern Workflow Strategies
Workflow 1: Brick Wall Generator
Complete Brick Pattern System
CLASS BrickWallGenerator:
brickWidth = 60
brickHeight = 30
mortarThickness = 4
colorVariation = 15 // Hue degrees
damageLevel = 0.2 // 0-1
FUNCTION Generate(width, height, position):
rows = floor(height / (brickHeight + mortarThickness))
FOR row FROM 0 TO rows - 1:
// Calculate offset for running bond pattern
offset = (row % 2) * (brickWidth / 2)
// Calculate how many bricks fit
bricksInRow = ceil(width / (brickWidth + mortarThickness)) + 1
FOR col FROM 0 TO bricksInRow - 1:
brickX = position.x + offset + col * (brickWidth + mortarThickness)
brickY = position.y + row * (brickHeight + mortarThickness)
// Generate varied brick
GenerateBrick(brickX, brickY)
END FOR
END FOR
// Add mortar
GenerateMortar(position, width, height)
END FUNCTION
FUNCTION GenerateBrick(x, y):
// Base color with variation
baseHue = 15 // Reddish
hue = baseHue + Random(-colorVariation, colorVariation)
sat = 0.6 + Random() * 0.2
bright = 0.4 + Random() * 0.2
brickColor = HSL(hue, sat, bright)
// Draw base brick
DrawRectangle(x, y, brickWidth, brickHeight, brickColor)
// Add texture
AddBrickTexture(x, y, brickWidth, brickHeight)
// Maybe add damage
IF Random() < damageLevel:
AddBrickDamage(x, y, brickWidth, brickHeight)
END IF
// Add lighting (top lighter, bottom darker)
AddBrickLighting(x, y, brickWidth, brickHeight)
END FUNCTION
FUNCTION AddBrickTexture(x, y, width, height):
// Add small details
detailCount = Random(5, 15)
FOR i FROM 0 TO detailCount:
dx = x + Random() * width
dy = y + Random() * height
size = Random(1, 3)
// Darker spots
detailColor = RGB(
Random(100, 140),
Random(60, 100),
Random(40, 80)
)
DrawSpot(dx, dy, size, detailColor, opacity: 0.3)
END FOR
// Add subtle grain
FOR gx FROM x TO x + width STEP 2:
FOR gy FROM y TO y + height STEP 2:
IF Random() > 0.7:
noise = PerlinNoise(gx * 0.1, gy * 0.1)
alpha = abs(noise) * 0.1
DrawPixel(gx, gy, RGB(0,0,0), alpha)
END IF
END FOR
END FOR
END FUNCTION
FUNCTION AddBrickDamage(x, y, width, height):
damageType = Random()
IF damageType < 0.4:
// Chip in corner
chipSize = Random(3, 8)
corner = RandomChoice(["topLeft", "topRight", "bottomLeft", "bottomRight"])
DrawChip(x, y, width, height, corner, chipSize)
ELSE IF damageType < 0.7:
// Crack
startX = x + Random() * width
DrawCrack(startX, y, startX + Random(-5,5), y + height)
ELSE:
// Weathering stain
stainX = x + Random() * width
stainY = y + Random() * height
DrawWeatheringStain(stainX, stainY, size: Random(5,15))
END IF
END FUNCTION
FUNCTION AddBrickLighting(x, y, width, height):
// Top highlight
highlightGradient = CreateGradient(
start: {x: x, y: y},
end: {x: x, y: y + height * 0.3},
startColor: RGB(255,255,255),
endColor: TRANSPARENT
)
DrawGradient(highlightGradient, opacity: 0.1)
// Bottom shadow
shadowGradient = CreateGradient(
start: {x: x, y: y + height * 0.7},
end: {x: x, y: y + height},
startColor: TRANSPARENT,
endColor: RGB(0,0,0)
)
DrawGradient(shadowGradient, opacity: 0.15)
END FUNCTION
END CLASS
Workflow 2: Crowd Generator
Procedural Crowd System
CLASS CrowdGenerator:
FUNCTION Generate(area, density, perspective):
// Calculate positions based on perspective
positions = GenerateCrowdPositions(area, density, perspective)
// Sort by depth (paint back to front)
positions.sortBy(position => position.y)
// Generate each person
FOR EACH position IN positions:
GeneratePerson(position, perspective)
END FOR
END FUNCTION
FUNCTION GenerateCrowdPositions(area, density, perspective):
positions = []
targetCount = area.width * area.height * density * 0.001
// Use Poisson disk for natural spacing
minDistance = 15 / density
WHILE positions.length < targetCount:
candidate = {
x: area.x + Random() * area.width,
y: area.y + Random() * area.height
}
// Adjust for perspective
candidate = ApplyPerspective(candidate, area, perspective)
// Check spacing
IF ValidSpacing(candidate, positions, minDistance):
positions.push(candidate)
END IF
END WHILE
RETURN positions
END FUNCTION
FUNCTION ApplyPerspective(position, area, perspective):
// More distant (higher y) = smaller, denser
depthFactor = (position.y - area.y) / area.height
// Apply perspective distortion
position.scaleMultiplier = 1.0 - depthFactor * perspective.strength
position.depth = depthFactor
RETURN position
END FUNCTION
FUNCTION GeneratePerson(position, perspective):
// Size based on depth
baseHeight = 40
height = baseHeight * position.scaleMultiplier
width = height * 0.4
// Random appearance
appearance = {
bodyColor: RandomClothingColor(),
hairColor: RandomHairColor(),
skinTone: RandomSkinTone(),
pose: RandomChoice(["standing", "walking", "sitting"])
}
// Simplified silhouette
IF height > 15: // Enough detail
DrawDetailedPerson(position, width, height, appearance)
ELSE: // Too small, just silhouette
DrawSimplifiedPerson(position, width, height, appearance)
END IF
// Add depth cues
ApplyDepthEffects(position, height)
END FUNCTION
FUNCTION DrawDetailedPerson(pos, width, height, appearance):
// Head
headSize = width * 0.5
DrawEllipse(pos.x, pos.y, headSize, headSize, appearance.skinTone)
// Hair
DrawHair(pos.x, pos.y, headSize, appearance.hairColor)
// Body
bodyY = pos.y + headSize
bodyHeight = height * 0.6
DrawRectangle(pos.x - width/2, bodyY, width, bodyHeight,
appearance.bodyColor)
// Legs
legY = bodyY + bodyHeight
legHeight = height * 0.4
legWidth = width * 0.4
DrawRectangle(pos.x - width/3, legY, legWidth, legHeight,
Darken(appearance.bodyColor, 0.3))
DrawRectangle(pos.x + width/10, legY, legWidth, legHeight,
Darken(appearance.bodyColor, 0.3))
END FUNCTION
FUNCTION DrawSimplifiedPerson(pos, width, height, appearance):
// Just a simple silhouette
avgColor = AverageColor(appearance.bodyColor, appearance.hairColor)
DrawEllipse(pos.x, pos.y + height/2, width/2, height/2, avgColor)
END FUNCTION
FUNCTION ApplyDepthEffects(position, height):
// Distant figures are less saturated and lighter
desaturation = position.depth * 0.4
lightening = position.depth * 0.3
blur = position.depth * 2
ApplyEffect(position, {
desaturate: desaturation,
lighten: lightening,
blur: blur
})
END FUNCTION
END CLASS
Workflow 3: Foliage Distribution
🌿 Smart Foliage Placement System
CLASS FoliageDistributionSystem:
FUNCTION Distribute(terrain, foliageTypes, rules):
// Analyze terrain for placement suitability
suitabilityMap = AnalyzeTerrain(terrain, rules)
// Place each foliage type
FOR EACH foliageType IN foliageTypes:
PlaceFoliageType(terrain, foliageType, suitabilityMap)
END FOR
END FUNCTION
FUNCTION AnalyzeTerrain(terrain, rules):
suitabilityMap = CreateMap(terrain.size)
FOR EACH point IN terrain:
suitability = 0
// Factor 1: Slope (trees prefer flat areas)
slope = CalculateSlope(point, terrain)
IF slope < rules.maxSlope:
suitability += (1.0 - slope / rules.maxSlope) * 0.3
END IF
// Factor 2: Elevation (different plants at different heights)
elevation = terrain.GetElevation(point)
elevationSuitability = CalculateElevationSuitability(
elevation,
rules.preferredElevation,
rules.elevationTolerance
)
suitability += elevationSuitability * 0.3
// Factor 3: Existing vegetation (clumping)
nearbyVegetation = CountNearbyVegetation(point, radius: 50)
IF nearbyVegetation > 0 AND nearbyVegetation < 5:
suitability += 0.2 // Good - some neighbors but not crowded
ELSE IF nearbyVegetation >= 5:
suitability -= 0.3 // Too crowded
END IF
// Factor 4: Distance from water
distToWater = DistanceToWater(point, terrain)
IF distToWater < rules.waterProximityPreference:
suitability += 0.2
END IF
suitabilityMap[point] = Clamp(suitability, 0, 1)
END FOR
RETURN suitabilityMap
END FUNCTION
FUNCTION PlaceFoliageType(terrain, foliageType, suitabilityMap):
// Generate candidates
candidates = []
targetCount = terrain.area * foliageType.density
FOR attempt FROM 0 TO targetCount * 5:
// Sample position weighted by suitability
position = SampleBySuitability(suitabilityMap)
// Check if valid
IF IsValidFoliagePlacement(position, foliageType, terrain):
candidates.push(position)
IF candidates.length >= targetCount:
BREAK
END IF
END IF
END FOR
// Place foliage at valid positions
FOR EACH position IN candidates:
PlaceFoliageInstance(position, foliageType, terrain)
END FOR
END FUNCTION
FUNCTION PlaceFoliageInstance(position, foliageType, terrain):
// Vary size based on local conditions
fertility = CalculateFertility(position, terrain)
size = foliageType.baseSize * (0.7 + fertility * 0.6)
// Vary appearance
hueShift = Random(-foliageType.colorVariation,
foliageType.colorVariation)
color = ShiftHue(foliageType.baseColor, hueShift)
// Rotate for variety
rotation = Random(0, TWO_PI)
// Draw using appropriate method
IF foliageType.type == "tree":
DrawTree(position, size, color, rotation)
ELSE IF foliageType.type == "bush":
DrawBush(position, size, color)
ELSE IF foliageType.type == "grass":
DrawGrassPatch(position, size, color, rotation)
END IF
// Update terrain data
RegisterVegetation(position, foliageType, terrain)
END FUNCTION
END CLASS
💡 Workflow Principle: The best pattern-based workflows are tunable. Expose parameters (density, variation, rules) so you can adjust the result without rewriting code. Think like a game designer - give yourself sliders and controls!
Master Project: Procedural Landscape Generator 🏆
Time to synthesize everything you've learned! Create a complete procedural landscape generator that can create diverse, detailed landscapes with a single button press - then customize them artistically!
🎯 Project Overview
Your Mission: Build a comprehensive landscape generation system that combines terrain generation, vegetation placement, atmospheric effects, and artistic refinement - all driven by procedural algorithms!
🌄 System Requirements
- ✅ Terrain generation using fractal algorithms
- ✅ Automatic vegetation distribution with biomes
- ✅ Water body generation (rivers, lakes)
- ✅ Sky and atmospheric effects
- ✅ Detail generation system
- ✅ Manual refinement tools
- ✅ Export multiple variations
Core Components to Implement
Component 1: Terrain Generator
Must Include:
- FBM or ridged multifractal for terrain height
- Adjustable parameters (roughness, height scale, features)
- Erosion simulation (optional but recommended)
- Multiple terrain types (mountains, hills, plains, valleys)
- Smooth transitions between terrain types
Deliverable: System that generates varied terrain from seed values
Component 2: Biome System
Must Include:
- At least 4 biome types (forest, desert, grassland, tundra/alpine)
- Biome placement based on elevation and climate
- Smooth transitions between biomes
- Biome-specific vegetation types
- Color palettes per biome
Deliverable: Varied landscapes with distinct ecological zones
Component 3: Vegetation Generator
Must Include:
- L-System or fractal trees (at least 2 species)
- Grass/ground cover system
- Bushes and shrubs
- Intelligent placement (respect terrain, clustering)
- LOD system (distant vegetation simplified)
- Variation in size, color, and form
Deliverable: Rich, varied vegetation that looks natural
Component 4: Water System
Must Include:
- Automatic water body placement in low areas
- Rivers following terrain flow
- Water reflections (simplified)
- Shoreline detail
- Different water colors (clear, murky, deep)
Deliverable: Believable water features integrated with terrain
Component 5: Atmospheric System
Must Include:
- Sky generation with gradient
- Procedural clouds (FBM-based)
- Atmospheric perspective application
- Fog/mist in valleys
- Time-of-day variations (dawn, noon, sunset)
Deliverable: Convincing atmosphere and depth
Component 6: Detail Generator
Must Include:
- Rock scatter system
- Ground texture detail
- Foreground detail enhancement
- Edge detail along ridges
- Manual detail brush tools
Deliverable: Rich detail that draws the eye
Implementation Phases
Phase 1: Foundation (Week 1)
- Implement core terrain generation
- Create basic biome system
- Test multiple seeds for variety
- Establish parameter system
Phase 2: Population (Week 2)
- Implement vegetation generators
- Create placement algorithms
- Add water body generation
- Test biome-vegetation interactions
Phase 3: Atmosphere (Week 3)
- Implement sky and cloud systems
- Add atmospheric effects
- Implement time-of-day system
- Refine depth and perspective
Phase 4: Detail & Polish (Week 4)
- Add detail generation systems
- Create manual refinement tools
- Optimize performance
- Generate portfolio variations
Deliverables
What to Submit
1. Working Generator System
- Complete procedural landscape generator
- Parameter control interface (sliders, seeds)
- Random generation capability
- Manual refinement tools
2. Generated Landscapes (8-10 variations)
- Show diversity of system capabilities
- At least 2 per biome type
- Different times of day
- Different weather/atmospheric conditions
- High resolution (3000×2000+ pixels)
3. Technical Documentation
Document Structure:
├── System Overview
│ ├── Architecture diagram
│ ├── Component descriptions
│ └── Data flow explanation
│
├── Algorithm Documentation
│ ├── Terrain generation algorithm
│ ├── Vegetation placement rules
│ ├── Water flow calculation
│ └── Atmospheric rendering
│
├── Parameter Reference
│ ├── All adjustable parameters
│ ├── Value ranges and effects
│ └── Recommended presets
│
├── Code Documentation
│ ├── Key algorithms with explanations
│ ├── Performance considerations
│ └── Optimization techniques used
│
└── Results & Analysis
├── Generated variations showcase
├── Strengths and limitations
├── Future improvements
└── Lessons learned
4. Video Demonstration (3-5 minutes)
- Show generation process from scratch
- Demonstrate parameter adjustments
- Show manual refinement tools
- Generate multiple variations live
5. Source Code & Assets
- Well-commented code
- Organized file structure
- Custom brushes used
- Texture libraries
- README with setup instructions
Evaluation Criteria
| Criteria | Weight | Evaluation Points |
|---|---|---|
| System Completeness | 25% |
• All required components implemented • Features work as specified • System is robust and reliable • Good parameter control |
| Algorithm Quality | 25% |
• Sophisticated algorithms used • Proper implementation of techniques • Good use of noise/fractals • Smart placement rules |
| Artistic Quality | 20% |
• Landscapes look natural and believable • Good composition and depth • Effective atmosphere • Aesthetically pleasing results |
| Variation & Diversity | 15% |
• System generates diverse results • Different biomes feel distinct • Sufficient randomness • Avoids repetitive patterns |
| Performance & Optimization | 10% |
• Generates in reasonable time • Efficient algorithms • LOD system implemented • No significant lag |
| Documentation | 5% |
• Clear technical documentation • Code well-commented • Good presentation • Video demonstration |
💡 Success Tips
- Start simple: Get basic terrain working before adding complexity
- Test frequently: Generate hundreds of variations to find edge cases
- Use references: Study real landscape photos for natural patterns
- Embrace happy accidents: Sometimes bugs create interesting features!
- Make it tunable: Every magic number should be a parameter
- Cache results: Pre-compute expensive operations
- Think in layers: Build complexity through composition
- Document as you go: Don't wait until the end!
🏆 Portfolio Impact: A working procedural landscape generator demonstrates mastery of algorithms, artistic vision, AND systems thinking. This is the kind of project that gets you noticed by game studios, VFX companies, and technical art teams!
Summary & Resources 🎓
🎯 Mastery Achievements Unlocked!
Congratulations on completing this journey into procedural art systems! You've gained skills that set you apart as a technical artist:
- ✅ Generative pattern algorithms
- ✅ Rule-based painting systems
- ✅ Automated detail generation
- ✅ Fractal mathematics (FBM, ridged)
- ✅ Noise functions (Perlin, Worley, turbulence)
- ✅ L-Systems for organic growth
- ✅ Pattern-based workflows
- ✅ Procedural placement algorithms
- ✅ Biome and ecosystem simulation
- ✅ Performance optimization for generation
- ✅ Parameter-driven creativity
- ✅ Complete landscape generation pipeline
Key Takeaways
🌀 The Procedural Mindset
"Procedural art isn't about replacing the artist - it's about multiplying their creative power. One set of rules can generate infinite variations. One algorithm can paint what would take hours manually. The artist becomes a director, orchestrating systems rather than painting every pixel."
Remember:
- Rules encode creativity: Well-designed rules capture artistic principles algorithmically
- Variation creates life: Pure randomness is chaos; controlled variation is natural
- Composition beats chaos: Layer simple systems to create complexity
- Parameters are power: Tunability turns algorithms into art tools
- Constraints breed creativity: Working within algorithmic limits sparks innovation
- Performance matters: A slow generator isn't useful, no matter how good
- Document everything: Future you won't remember why that magic number is 0.847
Advanced Resources
📚 Essential Reading
Procedural Generation:
- "Procedural Generation in Game Design" by Tanya X. Short & Tarn Adams
- "The Algorithmic Beauty of Plants" by Przemyslaw Prusinkiewicz
- "Texturing and Modeling: A Procedural Approach" (Perlin, Ebert, et al.)
- "The Nature of Code" by Daniel Shiffman (free online)
Fractals & Noise:
- "The Fractal Geometry of Nature" by Benoit Mandelbrot
- Ken Perlin's original papers on Perlin and Simplex noise
- "Noise and Turbulence" by Ken Musgrave
L-Systems:
- "The Algorithmic Beauty of Plants" (Prusinkiewicz & Lindenmayer)
- "L-Systems Notes" - various academic papers
🔗 Online Resources
Interactive Learning:
- Shadertoy.com - Explore procedural shaders
- The Book of Shaders - Free online noise/procedural tutorial
- OpenProcessing.org - Share and learn procedural sketches
Tools & Experiments:
- Processing / p5.js - Quick procedural prototyping
- Context Free Art - Grammar-based art tool
- Houdini - Professional procedural 3D software
- Substance Designer - Node-based procedural textures
Communities:
- r/proceduralgeneration - Reddit community
- Procedural Generation Discord servers
- Technical Art communities
What's Next?
🚀 Continue Your Journey
Immediate Challenges:
- Complete the procedural landscape generator project
- Create specialized generators (cities, dungeons, creatures)
- Experiment with domain warping and advanced noise
- Build a procedural portrait/character generator
- Create animated procedural effects
Advanced Topics:
- Wave Function Collapse algorithm
- Grammar-based generation systems
- Machine learning for pattern generation
- Procedural animation systems
- Real-time terrain generation
- Procedural music visualization
Module 2 Preview:
You've mastered Module 1: Master-Level Brush Engineering! Next, we'll dive into Module 2: Industry-Specific Pipelines, where you'll learn production workflows for game art, film/animation, and publishing. You'll take your procedural skills and apply them to real-world professional scenarios!
🎓 From User to Creator to Inventor
You started Module 1 learning brush physics. You learned to simulate traditional media. Now you've learned to create entire worlds with algorithms. This progression - from understanding tools, to creating tools, to inventing systems - is the mark of a master technical artist.
Procedural systems are the future of art production. Game worlds, movie effects, architectural visualization - they all rely on artists who can encode creativity into algorithms. You now have that power. Use it wisely, use it creatively, and most importantly - use it to make art that couldn't exist any other way!
🌟 Share Your Procedural Creations!
When you complete your landscape generator (or any procedural system), share it! Tag with #ProceduralArtSystems and #GenerativeArt
The procedural art community loves seeing new approaches and clever algorithms!
🎉 Module 1 Complete!
You've completed all three lessons in Master-Level Brush Engineering!