-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathBlogStateVisualization.html
More file actions
270 lines (234 loc) · 8.87 KB
/
Copy pathBlogStateVisualization.html
File metadata and controls
270 lines (234 loc) · 8.87 KB
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
<!DOCTYPE html>
<html>
<head>
<title>Encino Sim Class : State Visualization</title>
<style type="text/css" title="currentStyle">
@import "css/SimClass.css";
</style>
<script src='http://processingjs.org/js/processing.min.js' type='text/javascript'/>
</script>
<!script src="processing-1.4.1.js" type="text/javascript"/>
<!/script>
</head>
<body>
<h2>Visualizing the Simulation State</h2>
Once we've established the state vector for our system, the very first thing
that a simulation designer should do is to establish a visualization method
for as many parts of the simulation state as possible. The better the
visualization tools, the better the simulation, and the less time you will
spend tracking down bugs or chasing misbehaving physics.
<p>
In fact, good visualization tools will allow you to learn and understand
how your simulation is working at a deep level, and you'll begin to establish
an intuitive relationship with the measurements that form your simulation state.
<h2>Drawing the Pendulum</h2>
The first thing we do when preparing to draw is to define a transformation
from the simulation coordinate system to the viewing coordinate system. This
is a standard part of graphics pipelines and will be familiar to anyone
who has used OpenGL - we're essentially defining the transformation and
projection matrices.
In our pendulum case (and indeed in many of our examples), we simply need to
define a relationship between the working units of the sim, in this case
meters, and the pixels of the display. We define these variables in the
header:
<pre><code>
int WindowWidthHeight = 300;
float WorldSize = 2.0;
float PixelsPerMeter;
float OriginPixelsX;
float OriginPixelsY;
</code></pre>
and then in the setup function, we calculate their values. In this case
I'm explicitly deciding to put the origin at the midpoint in X, and a quarter
of the way down in Y.
<pre><code>
PixelsPerMeter = (( float )WindowWidthHeight ) / WorldSize;
OriginPixelsX = 0.5 * ( float )WindowWidthHeight;
OriginPixelsY = 0.25 * ( float )WindowWidthHeight;
</code></pre>
It is typical to try to construct viewing transformation, such as scaling
and translating the view, independently of modeling transformations. However,
processing.js has a bug with concatenated transformations, so unfortunately
our code has to accomodate the transformation from sim coordinates in meters
to pixel coordinates by itself.
<p>
We define a DrawState function that we call from processing's 'draw' function,
and draw the elements of our state. In this case we have a pendulum arm, a
pendulum bob (the weight at the end), and the pivot point at the base of the
pendulum. We use the angle of the pendulum from the state to determine the
endpoint of the arm for drawing, thus visualizing our state:
<pre><code>
// The DrawState function assumes that the coordinate space is that of the
// simulation - namely, meters, with the pendulum pivot placed at the origin.
// Draw the arm, pivot, and bob!
// There is currently a bug in processing.js which requires us to do the
// pixels-per-meter scaling ourselves.
void DrawState()
{
// Compute end of arm.
float ArmEndX = PixelsPerMeter * PendulumLength * sin( StatePendulumTheta );
float ArmEndY = PixelsPerMeter * PendulumLength * cos( StatePendulumTheta );
// Draw the pendulum arm.
strokeWeight( 1.0 );
line( 0.0, 0.0, ArmEndX, ArmEndY );
// Draw the pendulum pivot
fill( 0.0 );
ellipse( 0.0, 0.0,
PixelsPerMeter * 0.03,
PixelsPerMeter * 0.03 );
// Draw the pendulum bob
fill( 1.0, 0.0, 0.0 );
ellipse( ArmEndX, ArmEndY,
PixelsPerMeter * 0.1,
PixelsPerMeter * 0.1 );
}
</code></pre>
And that's all we need to display the state our our simple pendulum
simulation! Here it is running in processing. Note that there's no movement
yet, as we haven't added any time integration into the code - we've just
defined our state vector and a display mechanism for it.
<p>
<script type="application/processing" data-processing-target="pjsPendSimp">
float PendulumLength = 1.0;
float PendulumInitTheta = radians( 30.0 );
float StatePendulumTheta = radians( 30.0 );
float StatePendulumDthetaDt = 0.0;
int WindowWidthHeight = 300;
float WorldSize = 2.0;
float PixelsPerMeter;
float OriginPixelsX;
float OriginPixelsY;
void setup()
{
// Set up normalized colors.
colorMode( RGB, 1.0 );
// Set up the stroke color and width.
stroke( 0.0 );
//strokeWeight( 0.01 );
// Create the window size, set up the transformation variables.
size( WindowWidthHeight, WindowWidthHeight );
PixelsPerMeter = (( float )WindowWidthHeight ) / WorldSize;
OriginPixelsX = 0.5 * ( float )WindowWidthHeight;
OriginPixelsY = 0.25 * ( float )WindowWidthHeight;
}
// The DrawState function assumes that the coordinate space is that of the
// simulation - namely, meters, with the pendulum pivot placed at the origin.
// Draw the arm, pivot, and bob!
// There is currently a bug in processing.js which requires us to do the
// pixels-per-meter scaling ourselves.
void DrawState()
{
// Compute end of arm.
float ArmEndX = PixelsPerMeter * PendulumLength * sin( StatePendulumTheta );
float ArmEndY = PixelsPerMeter * PendulumLength * cos( StatePendulumTheta );
// Draw the pendulum arm.
strokeWeight( 1.0 );
line( 0.0, 0.0, ArmEndX, ArmEndY );
// Draw the pendulum pivot
fill( 0.0 );
ellipse( 0.0, 0.0,
PixelsPerMeter * 0.03,
PixelsPerMeter * 0.03 );
// Draw the pendulum bob
fill( 1.0, 0.0, 0.0 );
ellipse( ArmEndX, ArmEndY,
PixelsPerMeter * 0.1,
PixelsPerMeter * 0.1 );
}
// The draw function creates a transformation matrix between pixel space
// and simulation space, in meters, and then calls the DrawState function.
// Unfortunately, there is currently a bug in processing.js with concatenated
// transformation matrices, so we have to do the coordinate scaling ourselves
// in the draw function.
void draw()
{
background( 0.75 );
// Translate to the origin.
translate( OriginPixelsX, OriginPixelsY );
// Draw the simulation
DrawState();
}
</script>
<canvas id="pjsPendSimp"> </canvas>
<h2>Drawing the Wave Field</h2>
Drawing our other simple state example, a 1D Wave Height Field, is extremely
similar to drawing the pendulum state. We have a DrawState function, which again
due to bugs in Processing, does its own coordinate space transformations.
<p>
In this case, we just draw a rectangle in the world for each column of the
height field state. We leave a stroke around the rectangles so we can see each
individual state location. Here's what that draw function looks like:
<pre><code>
void DrawState()
{
float OffsetY = 0.5 * ( float )WindowHeight;
for ( int i = 0; i < ArraySize; ++i )
{
float SimX = DX * ( float )i;
float PixelsX = ( float )( i * PixelsPerCell );
float SimY = StateHeight[i];
float PixelsY = SimY * (( float )PixelsPerCell ) / DX;
float PixelsMinY = OffsetY - PixelsY;
float PixelsHeight = (( float )WindowHeight) - PixelsMinY;
fill( 0.0, 0.0, 1.0 );
rect( PixelsX, OffsetY - PixelsY, PixelsPerCell, PixelsHeight );
}
}
</code></pre>
And here's what it looks like in processing. Again, no movement yet - we're
just working on creating a visualization for the state.
<p><br>
<script type="application/processing" data-processing-target="pjsWaveSimp">
float WorldSize = 10.0;
int ArraySize = 128;
float DX = WorldSize / ArraySize;
float[] StateHeight = new float[ArraySize];
int PixelsPerCell = 4;
int WindowWidth = PixelsPerCell * ArraySize;
int WindowHeight = WindowWidth / 2;
float snoise( float x )
{
return ( 2.0 * noise( x )) - 1.0;
}
float anoise( float x )
{
return ( -2.0 * abs( snoise( x )) ) + 1.0;
}
void setup()
{
noiseSeed( 0 );
for ( int i = 0; i < ArraySize; ++i )
{
float worldX = 2341.17 + DX * ( float )i;
StateHeight[i] = 0.5 * anoise( worldX * 0.0625 ) +
0.4 * anoise( worldX * 0.125 ) +
0.3 * anoise( worldX * 0.25 ) +
0.2 * anoise( worldX * 0.5 );
}
size( WindowWidth, WindowHeight );
colorMode( RGB, 1.0 );
strokeWeight( 0.5 );
}
void DrawState()
{
float OffsetY = 0.5 * ( float )WindowHeight;
for ( int i = 0; i < ArraySize; ++i )
{
float SimX = DX * ( float )i;
float PixelsX = ( float )( i * PixelsPerCell );
float SimY = StateHeight[i];
float PixelsY = SimY * (( float )PixelsPerCell ) / DX;
float PixelsMinY = OffsetY - PixelsY;
float PixelsHeight = (( float )WindowHeight) - PixelsMinY;
fill( 0.0, 0.0, 1.0 );
rect( PixelsX, OffsetY - PixelsY, PixelsPerCell, PixelsHeight );
}
}
void draw()
{
background( 0.9 );
DrawState();
}
</script>
<canvas id="pjsWaveSimp"> </canvas>
</body>