Marek's blog

Growing HPCG logo

Video of simulation of growth of simple trees restricted by environment created using L-systems. This animation was made to demonstrate power of my L-system framework Malsys and to have fun with L-systems. Whole simulation is one L-system with sub-L-systems for leaves and blooms.

Figure 1: Examples from this project

Summary

Unfortunately, I have never got time to write a detailed article about this project, sorry! Highlights of the project are:

  • Uses Malsys - Marek's L-systems to interpret a custom L-system that simulates growth of 3D trees constrained by an environmental map
  • Result of each iteration of the L-system is exported to 3D model as OBJ and MTL files.
  • Each OBJ+MTL is loaded and rendered in a third-party renderer.
  • Individual frames are assembled into a video.

Here is the final video.

L-system code

Code listing 1: L-system code that generated the above video.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// Frames Per Iteration (roughly)
let FPI = 12;
//let FPI = 24; -- only for detail rendering

let baseSpeed = 1 / FPI;

fun getSpeed() {
	return baseSpeed * random(0.6, 1.4);
}

fun getInitT() {
	return baseSpeed * random(0.8, 1.2);
}


lsystem Tree extends Polygons {

	//set debugInterpretation = true;
	//set interpretWhileDebug = true;

	// branching angles (yaw)
	//let d0 = 0; -- inlined
	let d1 = 77;
	let d2 = 181;
	let d3 = 249;

	fun getRndPitch() {
		return -40 + random(-2, 2);
	}

	fun getRndRoll(baseAagle) {
		return baseAagle + random(-4, 4);
	}

	fun getLeafColor() {
		return darken(#41FF32, random(0, 0.1));
	}

	fun getBloomColor() {
		return #6495ED;
	}

	fun getBloomRodColor() {
		return #FFFFFF;
	}

	fun getBloomSize() {
		return random(3, 4);
	}

	fun getBloomLeafCount() {
		return floor(random(6, 10));
	}

	//set reportStatistics = true;

	let baseBranchLen = 60;
	let genBranchMult = -3;
	let stemColor = #228B22;  // forest green

	// to distinguish between individual iterations
	set addSuffixToFileNames = true;

	set interpretEveryIteration = true;
	//set interpretEveryIterationFrom = 10;


	let maxGeneration = 7;  // 7 is optimal, set lower for debugging

	//set iterations = 15;
	let it = (maxGeneration + 11) * FPI;  // min +9 for good results
	set iterations = it;

	set initialAngle = 90;

	set symbols axiom =
		[ GoTo(40, 1, 260) S(1, maxGeneration - 1) ]  // *H
		[ GoTo(190, 1, 260) S(1, maxGeneration - 1) ]  // H*
		[ GoTo(320, 1, 270) S(1, maxGeneration + 2) ]  // P
		[ GoTo(520, 1, 260) S(1, maxGeneration + 2) ]  // *C
		[ GoTo(630, 1, 260) S(1, maxGeneration - 1) ]  // C*
		[ GoTo(790, 1, 120) S(1, maxGeneration - 1) ]  // C^
		[ GoTo(770, 1, 390) S(1, maxGeneration - 1) ]  // Cv
		[ GoTo(870, 1, 300) S(1, maxGeneration) ]  // >G<
		[ GoTo(1030, 1, 250) S(1, maxGeneration) ]  // G*
		;

	// tropism up to eliminate (minimize) branches growing down
	set tropismVector = {0, 1, 0};
	set tropismCoefficient = 0.4;

	//set cameraPosition = {659, 970, 270};
	//set cameraUpVector = {0.04, 0.18, -0.99};
	//set cameraTarget = {549, 98, 294};

	// for triangulating leafs
	set polygonTriangulationStrategy = maxDistance;  // 3
	// turn this off to speedup processing, it is not working properly anyways
	set detectPlanarPloygons = false;

	// Ignore all symbols which we do not need in context
	// this should speedup processing a bit.
	// The only symbol needed in context is: Leaf and Branch (for separation).
	set symbols contextIgnore = / ^
		TestEnvironment
		GrowingBranch FakeBranch GoTo;


	interpret Branch(gen, t) as DrawForward(
		baseBranchLen + genBranchMult * gen,
		// branch will grow to maximum width in 6 iterations
		0.5 + (5 / gen) * min(1, t / 6),
		stemColor);

	// when t == FPI, growing branch must be equal to branch
	interpret GrowingBranch(t, gen) as DrawForward(
		(baseBranchLen + genBranchMult * gen) * t,
		0.5 * t,
		lighten(stemColor, 0.2 * (1 - t)));

	// fake branch must have same length as normal branch
	interpret FakeBranch(gen) as MoveForward(
		baseBranchLen + genBranchMult * gen);

	//interpret TestEnvironment as DrawSphere(2);

	interpret Leaf as lsystem Leaf;
	interpret Bloom as lsystem Bloom;
	interpret ^ as Pitch;
	interpret / as Roll;
	interpret GoTo as GoTo;


	rewrite S(gen, maxGen) to FakeBranch(gen) TestEnvironment(gen, maxGen);

	// if environment is ok, rewrite to branch
	rewrite TestEnvironment(gen, maxGen)
		where gen < maxGen && canLive()
		to GrowingBranch(getInitT(), gen, maxGen, getSpeed());

	// out of environment, place leaf
	rewrite TestEnvironment(gen)
		to /(random(-10,10)) Leaf(getLeafColor(), getInitT(), gen, getSpeed());

	// cleanup fake branches
	rewrite FakeBranch to nothing;


	// branch growth
	rewrite GrowingBranch(t, gen, maxGen, tSpeed)
		where t < 1
		to GrowingBranch(min(1, t+tSpeed), gen, maxGen, tSpeed);

	// branch growth ended, branch
	rewrite GrowingBranch(t, gen, maxGen)
		with g = gen + 1
		to Branch(gen, getInitT())
			[ /(getRndRoll(0))  ^(getRndPitch()) S(g, maxGen) ]
			[ /(getRndRoll(d1)) ^(getRndPitch()) S(g, maxGen) ]
			[ /(getRndRoll(d2)) ^(getRndPitch()) S(g, maxGen) ]
			[ /(getRndRoll(d3)) ^(getRndPitch()) S(g, maxGen) ];


	// branch growth
	rewrite Branch(gen, t) to Branch(gen, t + baseSpeed);

	// four leafes to three leafes and bloom (50% chance)
	rewrite {[Leaf] [Leaf] [Leaf]} Leaf
		to Bloom(getBloomSize(), getBloomLeafCount(), getBloomColor(), getBloomRodColor(), getInitT(), getSpeed()) weight 1
		to nothing weight 1;

	// leaf growth
	rewrite Leaf(c, t, gen, speed) to Leaf(c, t + speed, gen, speed);

	// bloom growth
	rewrite Bloom(s, l, c, rc, t, speed) to Bloom(s, l, c, rc, t + speed, speed);
}


abstract lsystem Leaf(color, t, gen) extends Polygons {

	let growthMult = min(1, t);  // goes from 0 to 1 first generation, stops at 1

	// max 14 iterations
	set iterations = 14;

	// older leafes will be smaller
	// leaf will grow in 5 iterations
	let scale = (0.2 + (1 / gen)) * min(1, t / 5);
	let angle = 2;

	let LA = 5;  // length - main segment
	let RA = 1;  // growth rate - main segment
	let LB = 1;  // initial length - lateral segment
	let RB = 1.2;  // growth rate - lateral segment
	let PD = 1;  // growth potential decrement

	let stemLen = 10*scale*growthMult;
	let stemWid = 2*scale*growthMult;
	let stemColor = darken(color, 0.3);

	set symbols axiom = ^(-30)
		F(stemLen * growthMult, stemWid * growthMult, stemColor) X(-stemLen * growthMult)
		<(color) . A(0) >;

	interpret F as DrawForward;
	interpret X as MoveForward;
	interpret G(len) as MoveForward(len*scale);
	interpret + as Yaw(60);
	interpret - as Yaw(-60);
	interpret ^ as Pitch(angle);

	rewrite A(t) to G(LA,RA) [ - B(t) . ] [ ^ A(t+1) ] [ + B(t) . ];
	rewrite B(t) where t > 0 to G(LB,RB) B(t-PD);
	rewrite G(s, r) to G(s*r,r);

}

abstract lsystem LeafTest {

	set polygonTriangulationStrategy = minAngle;  // 1

	set interpretEveryIteration = true;
	set symbols axiom = /(90) L(0);
	set iterations = 20;

	set scale = 10;

	interpret L as lsystem Leaf;
	interpret / as Roll;

	rewrite L(x) to L(x + 1);

}

/// Optimal base size is 3
abstract lsystem Bloom(baseSize, leafCount, color, rodColor, t) extends Polygons {

	let growthMult = min(1, t);  // goes from 0 to 1 first generation, stops at 1

	let darkerColor = darken(color, 0.1);
	let angle = 200 / leafCount;
	let scale = min(baseSize, t);  // bloom will grow in 'baseSize' iterations

	let leafAngle = min(70, 15 * t) + 10; // bloom will open in five iterations
	let rodAngleMult = min(1, t / 5); // rods will open in five iterations

	let rodCount = 2 * leafCount;
	let rodDarkerColor = darken(rodColor, 0.2);

	set symbols axiom = F [ G K ] F(-scale / 6)
		[ leaf(leafCount) ]
		[ rod(rodCount) ];
	set iterations = max(leafCount, rodCount);

	interpret F as DrawForward(scale * 2.5, (1 + scale / 4) * growthMult, color);
	interpret f as MoveForward(scale);
	interpret r as MoveForward(2 * scale);
	interpret s as MoveForward(scale / 4);
	//interpret K as DrawSphere(scale / 2, #ffff00);
	interpret K as DrawForward(scale/2, (1 + scale/5) * growthMult, #ffff00);

	interpret + as Yaw(angle);
	interpret - as Yaw(-angle / 2);
	interpret | as Yaw(180);
	interpret / as Roll;

	interpret ^ as Pitch(-10);

	rewrite leaf(num) where num > 0 to
		/(360 / leafCount) [ ^(leafAngle) <(color) .
		+ ^ f . - ^ f . - ^ f . - ^ f . - ^ f . + |
		+ f . - ^ f . - ^ f . - ^ f .  > ] leaf(num - 1);

	rewrite rod(num) where num > 0 to
		/(360 / rodCount) <(rodColor)
		[ ^((40 + (num % 2) * 10) * rodAngleMult) r . ] [ +(90) s . ] [ +(-90) s . ] >
		rod(num - 1);
}


abstract lsystem BloomTest extends Branches {

	set polygonTriangulationStrategy = minAngle;  // 1

	set interpretEveryIteration = true;
	set symbols axiom = [ +(90) B(8, 0) ] F(10) B(8, 0);
	set iterations = 16;

	set scale = 10;

	interpret B as lsystem Bloom;
	interpret F as MoveForward;
	interpret + as Yaw;

	rewrite B(l, x) to B(l, x + 1);

}



lsystem PlainTree extends Tree {

	set symbols axiom = S(1, maxGeneration);

	set iterations = (maxGeneration + 2) * FPI;

	rewrite S(gen, maxGen)
		where gen < maxGen
		to GrowingBranch(getInitT(), gen, maxGen, getSpeed());

	rewrite S to nothing;

}


lsystem CutTree extends Tree {

	set symbols axiom = /(-40) S(1, maxGeneration);

	set iterations = (maxGeneration + 4) * FPI;

	// do not add leafes when branches are too old
	rewrite TestEnvironment(gen, maxGen)
		where gen >= maxGen
		to nothing;

	rewrite TestEnvironment(gen, maxGen)
		where getEnvPosition()[2] < 50
		to GrowingBranch(getInitT(), gen, maxGen, getSpeed());

	rewrite TestEnvironment(gen)
		to /(random(-10,10)) Leaf(getLeafColor(), getInitT(), gen, getSpeed());

	// Force leaf rewriting to not create blooms
	rewrite Leaf(c, t, gen, speed) to Leaf(c, t + speed, gen, speed);

}

lsystem DetailTree extends Tree {

	set symbols axiom =
		[ GoTo(190, 1, 260) S(1, maxGeneration - 1) ]  // H*
		[ GoTo(320, 1, 270) S(1, maxGeneration + 2) ]  // P
		[ GoTo(520, 1, 260) S(1, maxGeneration + 2) ]  // *C
		;

	set iterations = it * 3 / 4;
	set interpretEveryIteration = false;
	set interpretEveryIterationFrom = it / 3;

}

// needs different heightmap
lsystem Heart extends Tree {

	set symbols axiom =
		[ GoTo(160, 1, 130) S(1, maxGeneration) ]
		;
	rewrite {[Leaf] [Leaf] [Leaf]} Leaf
		to Bloom(getBloomSize() * 1.1, getBloomLeafCount(), #FF0000, #AA0000, getInitT(), getSpeed());
}


process Tree with EnvRenderer
	use ObjExporter3D as Renderer  // comment-out to display scene using WebGL
	//use GdiBitmapRenderer as Renderer
;

//process LeafTest with BitmapRenderer;
//process BloomTest with BitmapRenderer;
//process PlainTree with ObjExporter;
//process CutTree with EnvRenderer use ObjExporter3D as Renderer;

// set FPI to 24 for following L-system
//process DetailTree with EnvRenderer use ObjExporter3D as Renderer;

// needs heart heightmap
process Heart with EnvRenderer use ObjExporter3D as Renderer;


configuration EnvRenderer {

	component Environment typeof SimpleEnvironmentModule;
	component EnvInterpreter typeof TurtleInterpreter;
	component EnvInterpreterCaller typeof InterpreterCaller;
	component EmptyRenderer typeof EmptyRenderer;

	container Rewriter typeof IRewriter default SymbolRewriter;
	container Iterator typeof IIterator default MemoryBufferedIterator;

	// the connection is unituitecely reversed, but this order is because of enumerators
	connect Environment to Rewriter.SymbolProvider;
	connect Iterator to Environment.SymbolProvider;
	connect Rewriter to Iterator.SymbolProvider;


	connect EnvInterpreterCaller to Environment.InterpreterCaller;
	connect EnvInterpreter to Environment.TurtleInterpreter;
	connect EnvInterpreter to EnvInterpreterCaller.ExplicitInterpreters;
	connect EmptyRenderer to EnvInterpreter.Renderer;


	container Interpreter typeof IInterpreter default TurtleInterpreter;
	container InterpreterCaller typeof IInterpreterCaller default InterpreterCaller;

	connect Interpreter to InterpreterCaller.ExplicitInterpreters;


	component LsystemInLsystemProcessor typeof LsystemInLsystemProcessor;
	component AxiomProvider typeof AxiomProvider;
	component RandomGeneratorProvider typeof RandomGeneratorProvider;

	container Renderer typeof IRenderer default ThreeJsSceneRenderer3D;

	connect RandomGeneratorProvider to Iterator.RandomGeneratorProvider;
	connect AxiomProvider to Iterator.AxiomProvider;
	connect LsystemInLsystemProcessor to InterpreterCaller.LsystemInLsystemProcessor;
	connect Renderer to Interpreter.Renderer;

	component SymbolsSaver typeof SymbolsSaver;
	// interpreter vs symbols
	connect InterpreterCaller to Iterator.OutputProcessor;
	//connect SymbolsSaver to Iterator.OutputProcessor;

}
This post is licensed under CC BY 4.0 by the author.

© Marek Fiser. Some rights reserved.

Inspired by the Chirpy theme despite not using Jekyll.