Thursday, October 27, 2005
objects needing more comments
ctrlresize, replacefilesdlg, masterdlg, parmrow, playlist, numbersdlg, viewdialog
Tuesday, October 25, 2005
adding HLS color to existing snapshots
ar << sizeof(RING);
ar << m_Ring.GetCount();
POSITION pos = m_Ring.GetHeadPosition();
for (int i = 0; i < m_Ring.GetCount(); i++) {
RING rp = m_Ring.GetNext(pos);
double h, l, s;
int c = rp.Color;
CHLS::rgb2hls(GetRValue(c) / 255.0,
GetGValue(c) / 255.0, GetBValue(c) / 255.0, h, l, s);
rp.Hue = h;
rp.Lightness = l;
rp.Saturation = s;
ar.Write(&rp, sizeof(RING));
}
Monday, October 24, 2005
Movie format could save space
The movies are using the snapshot format, which includes all of the ring info. In the ring info, the rotation and shift deltas and the HLS color are not used at all during movie playback. These attributes constitute 40 bytes out of 120, or one third of the total size of the ring structure. The actual savings could be nearly a third, since most of the space in a movie is taken up by rings (hundreds of rings per frame).
The savings would require adding a second serialization method, e.g. MiniSerialize, which would be used by the SnapMovie object. The method could be optimized by placing the deltas and the HLS color at the start of the ring structure, because then it would still be possible to do a single write directly from the ring, instead of copying to an intermediate structure first.
A problem with this approach is that it would longer be possible to export a complete (non-mini) snapshot from a movie, since some of the information would be missing. The HLS color could be restored from RGB, but not the deltas. On the other hand the deltas aren't useful in snapshots so maybe it wouldn't matter.
NOTE that WinZip reduces Steve1.whm by 61%, and Steve2.whm by 73%.
Space-saving benchmarks:
60 seconds, 25 FPS, default speed, window maximized but not full-screen, wait for full ring count before starting to record, sizes in MB, ring counts are rough averages
useful nugget of ring conversion code:
The savings would require adding a second serialization method, e.g. MiniSerialize, which would be used by the SnapMovie object. The method could be optimized by placing the deltas and the HLS color at the start of the ring structure, because then it would still be possible to do a single write directly from the ring, instead of copying to an intermediate structure first.
A problem with this approach is that it would longer be possible to export a complete (non-mini) snapshot from a movie, since some of the information would be missing. The HLS color could be restored from RGB, but not the deltas. On the other hand the deltas aren't useful in snapshots so maybe it wouldn't matter.
NOTE that WinZip reduces Steve1.whm by 61%, and Steve2.whm by 73%.
Space-saving benchmarks:
60 seconds, 25 FPS, default speed, window maximized but not full-screen, wait for full ring count before starting to record, sizes in MB, ring counts are rough averages
patch rings old new less
kaleid 1000 172.5 108.8 37%
pinwheel 500 98.0 62.5 36%
lotus light 300 71.4 46.5 35%
cross 150 34.6 26.3 24%
useful nugget of ring conversion code:
nr.RotDelta = rp.RotDelta;
nr.ShiftDelta = rp.ShiftDelta;
nr.Hue = rp.Hue;
nr.Lightness = rp.Lightness;
nr.Saturation = rp.Saturation;
nr.Rot = rp.Rot;
nr.Steps = rp.Steps;
nr.Scale = rp.Scale;
nr.Shift = rp.Shift;
nr.StarRatio = rp.StarRatio;
nr.Sides = rp.Sides;
nr.Delete = rp.Delete;
nr.Reverse = rp.Reverse;
nr.Color = rp.Color;
nr.Pinwheel = rp.Pinwheel;
nr.LineWidth = rp.LineWidth;
nr.DrawMode = rp.DrawMode;
nr.Spacing = rp.Spacing;
Thursday, October 20, 2005
array of CArrays not optimized correctly
It looks like dereferencing an array of CArray objects can causes MFC to call the [] operator, instead of inlining it, even in Release mode. Presumably this is why:
generates twice as much code as:
Oddly, this is bad too:
but this is fine (1 line longer than original):
int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
for (int i = 0; i < Patches; i++) {
if (HotKey == m_Bank[m_CurBank][i].m_HotKey)
return(i);
}
return(-1);
}
generates twice as much code as:
int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
for (int i = 0; i < Patches; i++) {
if (HotKey == (*m_Patch)[i].m_HotKey)
return(i);
}
return(-1);
}
Oddly, this is bad too:
int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
const PATCH_LIST *p = &m_Bank[m_CurBank];
for (int i = 0; i < Patches; i++) {
if (HotKey == (*p)[i].m_HotKey)
return(i);
}
return(-1);
}
but this is fine (1 line longer than original):
int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
for (int i = 0; i < Patches; i++) {
if (HotKey == m_Bank[m_CurBank].GetData()[i].m_HotKey)
return(i);
}
return(-1);
}
Saturday, October 08, 2005
MainFrame bloat
2662 lines and counting, ouch
non-message handlers 1139
message map 166
message handlers 1200
things that could move easily:
ShowDemo: only 32 lines but could get bigger
not so easy
GetInput: 83 lines and could get bigger
non-message handlers 1139
message map 166
message handlers 1200
things that could move easily:
ShowDemo: only 32 lines but could get bigger
not so easy
GetInput: 83 lines and could get bigger
odd shift
double xshift[2] = {rp.Shift.x * m_st.Zoom, (rp.Shift.x + rp.OddShift.x) * m_st.Zoom};
double yshift[2] = {rp.Shift.y * m_st.Zoom, (rp.Shift.y + rp.OddShift.y) * m_st.Zoom};
Notice that the calculation is NOT corrected for radius. This causes the effect to decrease with distance from the origin. Correcting for radius gives a very different and less pleasing effect. The display becomes too busy, and at extreme values, the image is distorted into a narrow cylinder. The first objection could possibly be addressed by limiting the size of the ring list.
To correct the above for radius:
double xshift[2] = {rp.Shift.x * m_st.Zoom, (rp.Shift.x + rp.OddShift.x * rp.Steps) * m_st.Zoom};
double yshift[2] = {rp.Shift.y * m_st.Zoom, (rp.Shift.y + rp.OddShift.y * rp.Steps) * m_st.Zoom};
Some possible names for "odd shift":
Shear: incorrect usage
Extrude: incorrect usage
Tilt: too vague
Tunnel: too vague, also other parameters can produce this effect
Relief: too vague
Stamen: too obscure
Center Fold: misleading, the new version corrects for radius, see below
Splay: means legs spread at various angles, not various lengths
Deform: vague, not a noun, misleading negative connotations
Bias: misleading connotation of unfairness
Fold: vague, possibly misleading
Asymmetry: vague, too long a word
10/29/05 corrected for radius but also centered, much better
DPOINT splay = {rp.Splay.x * rp.Steps, rp.Splay.y * rp.Steps};
double xshift[2] = {
(rp.Shift.x - splay.x) * m_st.Zoom,
(rp.Shift.x + splay.x) * m_st.Zoom};
double yshift[2] = {
(rp.Shift.y - splay.y) * m_st.Zoom,
(rp.Shift.y + splay.y) * m_st.Zoom};
Controls should be polar as with Skew: Splay Radius, Splay Angle
Some interesting interdependent terms:
Contort: Twist, wrench, or bend severely out of shape
Pinch: To press, squeeze, or bind painfully
Buckle: Bending, warping, or crumpling; a bend or bulge
Rumple: An irregular or untidy crease
Crumple: To crush together or press into wrinkles; rumple
Crease: A line made by pressing, folding, or wrinkling
Wrinkle: A small furrow, ridge, or crease on a normally smooth surface
Pucker: To gather into small wrinkles or folds
And the winner as of 12/17/05 is:
PUCKER
totally cool demo: default patch, ring spacing 11.192, pucker radius -.406, pucker angle -65.88 / ramp up / 180 / .08, master speed 222
another one: patch_051028050234, rings = 75, pucker rad = .268, angle = ramp up / 180 / .5, speed = 495
optimized 12/19/05 (two less Zoom multiplications):
DPOINT pucker = {rp.Pucker.x * steps, rp.Pucker.y * steps};
DPOINT shift = {rp.Shift.x * m_st.Zoom, rp.Shift.y * m_st.Zoom};
double xshift[2] = {shift.x - pucker.x, shift.x + pucker.x};
double yshift[2] = {shift.y - pucker.y, shift.y + pucker.y};
ring list size limit
in AddRing, at the very end:
while (m_Ring.GetCount() > whatever) {
if (Ring.Reverse)
m_Ring.RemoveHead();
else
m_Ring.RemoveTail();
}
Bézier curves
in Draw:
#if 1
m_pa[i] = m_pa[0]; // close the shape
Polyline(dc, m_pa, sides + 1);
#else
CPoint pt2[200];
int j = 0;
for (int i = 0; i < sides; i++) {
pt2[j++] = m_pa[i];
if (i & 1)
pt2[j++] = CPoint(m_pa[i].x, m_pa[i].y);
}
pt2[j++] = pt2[0];
PolyBezier(dc, pt2, j);
#endif
Saturday, October 01, 2005
scene rotation
in DefState:
0 // Rotation
in Addring:
Ring.Rot = Ring.RotDelta * Offset + m_st.Rotation;
void CWhorldView::Rotate(double Degrees, bool Repaint)
{
POSITION pos = m_Ring.GetHeadPosition();
double r = DTR(Degrees);
while (pos != NULL) {
RING& Ring = m_Ring.GetNext(pos);
Ring.Rot += r;
}
m_st.Rotation = fmod(m_st.Rotation + r, 2 * PI); // wrap to limit magnitude
if (Repaint)
Invalidate();
}
Note that this causes unexpected behavior (addition, subtraction, cancelation) when rotation is inverse of LFO rotation. Also X/Y shifts are NOT rotated, so for example if Aspect Ratio is 2, the asymmetry will remain orthogonal instead of rotating. Solving this would require changes to the drawing code.
benchmarks for movie recording
------------
PIII, Kaleid, 1024 x 768, 30 FPS, speed = 680, 2500 samps: min = .001197, max = .004851, avg = .001821, sdev = .000362
avg .002 = .06 @ second = 6% avg usage for recording
-----------
AMD, Kaleid, 1024 x 768, 30 FPS, speed = 680, 2500 samps: min = .000167, max = .000611, avg = .000322, sdev = 8.8329E-05
avg .0003 = .009 @ second = .9% avg usage for recording
------------
AMD test repeated over 5 minutes: min = .000172, max = .005491, avg = .000485, sdev = .000129
all samples were < than 1 ms except for a single single spike of 0.005491 at 3:38
------------
PIII, Kaleid, 1024 x 768, 30 FPS, speed = 680, 2500 samps: min = .001197, max = .004851, avg = .001821, sdev = .000362
avg .002 = .06 @ second = 6% avg usage for recording
-----------
AMD, Kaleid, 1024 x 768, 30 FPS, speed = 680, 2500 samps: min = .000167, max = .000611, avg = .000322, sdev = 8.8329E-05
avg .0003 = .009 @ second = .9% avg usage for recording
------------
AMD test repeated over 5 minutes: min = .000172, max = .005491, avg = .000485, sdev = .000129
all samples were < than 1 ms except for a single single spike of 0.005491 at 3:38
------------
glitch #1: Jeff & Sean
Sean:
1. Visible tearing of image: DirectX is supposed to care of this no? Would full-screen mode affect it? Possibly affected by frame rate and CPU loading.
2. Should sync to beat automatically.
3. Should support multiple trackballs and other input devices at once.
Jeff:
1. Support for MIDI notes would be helpful since he's using an Oxygen 8. Notes could act as toggles for the function keys.
2. Tempo nudge should NOT resync, or at least there should be an alternate nudge that doesn't resync. Sean agrees.
3. Rotation of the entire image (as opposed to rotate speed), especially via MIDI.
4. Zoom should go further out: can do this with MIDI.
1. Visible tearing of image: DirectX is supposed to care of this no? Would full-screen mode affect it? Possibly affected by frame rate and CPU loading.
2. Should sync to beat automatically.
3. Should support multiple trackballs and other input devices at once.
Jeff:
1. Support for MIDI notes would be helpful since he's using an Oxygen 8. Notes could act as toggles for the function keys.
2. Tempo nudge should NOT resync, or at least there should be an alternate nudge that doesn't resync. Sean agrees.
3. Rotation of the entire image (as opposed to rotate speed), especially via MIDI.
4. Zoom should go further out: can do this with MIDI.
Wednesday, September 28, 2005
movie disk requirements
The data rate of a WHM movie file is linearly proportional to scene complexity (as measured by ring count), and is also affected by the frame rate. Typical data rates range from .4 - 3.0 MBytes @ second, which means a 1-hour recording can require anywhere from 1.5 to 10 GB.
Both ATA 100 and Firewire A can sustain write data rates of at least 30 MB @ second, with minimal CPU loading, so performance probably isn't an issue. Disk space could be an issue, particularly on older machines.
Using WinZip on a WHM file typically yields a 60% - 70% size reduction. Recording to a compressed folder yields a 40% - 60% reduction, but causes the display to freeze at regular intervals on a PIII machine. The Gnu zlib compression library might have less impact, but some increase in CPU load is inevitable. To reduce disk usage, it might make sense to compress WHM files after recording. The app could include an option that does this automatically, but it would have to be done from a separate low-priority thread, and even then performance might be affected by context switching.
A higher-order solution would record user input instead of recording ring data. This would reduce the data rate to a negligible amount, but at the expense of greatly increased complexity and fragility. This method should be explored for a future version.
A very rough estimate of the data rate resulting from input recording:
Record cursor delta for every frame: 8 bytes (2 ints) @ frame
Record 16 MIDI controllers for every frame (worst case): 48 bytes @ frame
Keyboard commands are too infrequent to affect the estimate
8 + 48 = 56 bytes per frame; at 30 FPS, 56 * 30 = 1680 bytes @ second
That's 6MB per hour in the absolute worst case
Some benchmarks:
Uncompressed 1024 x 768 video at 30 FPS: 70.5 MB @ sec
Lotus light, speed around 700, 30 FPS, 1 minute = 131 MB (2.2 MB @ sec)
Cross, speed around 700, 30 FPS, 1:36 = 80 MB (.83 MB @ sec)
Kaleid, speed around 700, 30 FPS, 1:36 = 282 MB (2.9 MB @ sec)
Another interesting possibility: Recording the view's oscillators and STATE member but not the rings, and then regenerating the rings on playback/export
1 oscillator = 44 bytes * 16 = 704 bytes @ frame
view STATE member = 104 bytes + 704 = 808 bytes @ frame
call it 1K bytes @ frame, at 30 FPS = 30K bytes @ second (108 MB @ hour)
Both ATA 100 and Firewire A can sustain write data rates of at least 30 MB @ second, with minimal CPU loading, so performance probably isn't an issue. Disk space could be an issue, particularly on older machines.
Using WinZip on a WHM file typically yields a 60% - 70% size reduction. Recording to a compressed folder yields a 40% - 60% reduction, but causes the display to freeze at regular intervals on a PIII machine. The Gnu zlib compression library might have less impact, but some increase in CPU load is inevitable. To reduce disk usage, it might make sense to compress WHM files after recording. The app could include an option that does this automatically, but it would have to be done from a separate low-priority thread, and even then performance might be affected by context switching.
A higher-order solution would record user input instead of recording ring data. This would reduce the data rate to a negligible amount, but at the expense of greatly increased complexity and fragility. This method should be explored for a future version.
A very rough estimate of the data rate resulting from input recording:
Record cursor delta for every frame: 8 bytes (2 ints) @ frame
Record 16 MIDI controllers for every frame (worst case): 48 bytes @ frame
Keyboard commands are too infrequent to affect the estimate
8 + 48 = 56 bytes per frame; at 30 FPS, 56 * 30 = 1680 bytes @ second
That's 6MB per hour in the absolute worst case
Some benchmarks:
Uncompressed 1024 x 768 video at 30 FPS: 70.5 MB @ sec
Lotus light, speed around 700, 30 FPS, 1 minute = 131 MB (2.2 MB @ sec)
Cross, speed around 700, 30 FPS, 1:36 = 80 MB (.83 MB @ sec)
Kaleid, speed around 700, 30 FPS, 1:36 = 282 MB (2.9 MB @ sec)
Another interesting possibility: Recording the view's oscillators and STATE member but not the rings, and then regenerating the rings on playback/export
1 oscillator = 44 bytes * 16 = 704 bytes @ frame
view STATE member = 104 bytes + 704 = 808 bytes @ frame
call it 1K bytes @ frame, at 30 FPS = 30K bytes @ second (108 MB @ hour)
Thursday, September 22, 2005
making a WMV-9 DVD
Toshi says no need to change frame rate for WMV; between 5 and 20 MBits per sec should be good, and 1024 x 768 is fine. Must compress the bitmaps first to make enough room to create an AVI, expect on the order of 75% compression overall.
Monday, September 12, 2005
additional MIDI control
on/off: mirror, fill, outline, xray, invert hue, invert fill, invert outline, invert xray, rotate hue, reverse
zoom should be damped, no?
zoom should be damped, no?
Saturday, September 10, 2005
show size in status bar message while resizing
You must handle WM_ENTERSIZEMOVE and wM_EXITSIZEMOVE to show the initial size and restore the default message.
Note that this only works if the system property "show window contents while dragging" is set. Otherwise you only see the initial size, because WM_SIZE isn't sent until the drag ends. WM_SIZING *is* sent, but it doesn't help, because it passes us the size of the frame, whereas we want the size of the view. Oddly, there seems to be no way to determine the client area that would result from a given window size. CalcWindowRect does the reverse: it gives us the size of the window needed for a given client area.
LRESULT CMainFrame::OnEnterSizeMove(WPARAM wParam, LPARAM lParam)
{
CRect r;
m_View->GetClientRect(r);
CString s;
s.Format("%d x %d\n", r.Width(), r.Height());
SetMessageText(s); // must do all the above in OnSize too, but only if window is visible
return(TRUE);
}
LRESULT CMainFrame::OnExitSizeMove(WPARAM wParam, LPARAM lParam)
{
SetMessageText(AFX_IDS_IDLEMESSAGE);
return(TRUE);
}
Note that this only works if the system property "show window contents while dragging" is set. Otherwise you only see the initial size, because WM_SIZE isn't sent until the drag ends. WM_SIZING *is* sent, but it doesn't help, because it passes us the size of the frame, whereas we want the size of the view. Oddly, there seems to be no way to determine the client area that would result from a given window size. CalcWindowRect does the reverse: it gives us the size of the window needed for a given client area.
Tuesday, September 06, 2005
CalcWindowRect bug
This bug first appeared during the development of movie export. Moving the export to a separate process made it go away, but now it's back in version 1.0.73. Most likely the AVI file DLL was falsely accused and the movie export could have been part of the main app, but that's a secondary issue.
The symptom is, the app crashes in RowDialogForm's CalcWindowRect during the construction of the Parms dialog. It doesn't crash in debug, only in release, and creating a console window makes it go away (great!). Deleting the unused m_Template member variable from CPersistDlg.h also makes it go away. This last symptom clearly points towards a memory corruption problem of some kind. In the release debugger, the problem begins when CRowDialog::OnInitDialog calls CViewDialog::OnInitDialog. After this call, CRowDialog's "this" and HWND are trashed. Note that neither CViewDialog nor its immediate base class (CPersistDlg) override OnInitDialog, so this is effectively a call to CDialog::OnInitDialog.
Removing the call to MakeVJAccelTable in CMainFrame's ctor also makes it go away.
The really bad news: the app only crashes when you execute it from the IDE; running from Explorer is OK.
Other observations:
in Frame's ctor list, m_ResultsDlg(m_View), is incorrect initialization, m_View isn't the dialog's parent and in any case it's NULL! deleting this line doesn't help though...
could be passing 'this' to dialogs isn't such a good idea after all, seems like CMainFrame's 'this' keeps changing somehow or is that just the debugger being bad in release mode?
The solution: in CalcWindowRect, casting GetParent() down to CRowDialog isn't always correct. Initially, the parent is the dummy frame, not CRowDialog. Use a dynamic downcast to determine the parent.
The moral of the story: Downcasting is dangerous so use dynamic downcast and verify the result!
The symptom is, the app crashes in RowDialogForm's CalcWindowRect during the construction of the Parms dialog. It doesn't crash in debug, only in release, and creating a console window makes it go away (great!). Deleting the unused m_Template member variable from CPersistDlg.h also makes it go away. This last symptom clearly points towards a memory corruption problem of some kind. In the release debugger, the problem begins when CRowDialog::OnInitDialog calls CViewDialog::OnInitDialog. After this call, CRowDialog's "this" and HWND are trashed. Note that neither CViewDialog nor its immediate base class (CPersistDlg) override OnInitDialog, so this is effectively a call to CDialog::OnInitDialog.
Removing the call to MakeVJAccelTable in CMainFrame's ctor also makes it go away.
The really bad news: the app only crashes when you execute it from the IDE; running from Explorer is OK.
Other observations:
in Frame's ctor list, m_ResultsDlg(m_View), is incorrect initialization, m_View isn't the dialog's parent and in any case it's NULL! deleting this line doesn't help though...
could be passing 'this' to dialogs isn't such a good idea after all, seems like CMainFrame's 'this' keeps changing somehow or is that just the debugger being bad in release mode?
The solution: in CalcWindowRect, casting GetParent() down to CRowDialog isn't always correct. Initially, the parent is the dummy frame, not CRowDialog. Use a dynamic downcast to determine the parent.
The moral of the story: Downcasting is dangerous so use dynamic downcast and verify the result!
Monday, August 29, 2005
movies
pjbmp2avi.exe seems to work fine, with the CinePack codec at 100% (default) it's about 11MB for 36 seconds.
Storing snapshots with window size at 720x480 (DVD fullscreen) yields 80K snaps on average, with no appreciable slowdown: 80 * 25 * 60 = 120MB @ minute. This seems better than storing bitmaps.
Actual date rate test: demo speed = 975, 18.4 minutes, size 1.256 GB, rate = 73MB @ minute
Random waveform bug can be demonstrated using new OscTest, settings: random, .01, .5, .05, .1
Storing snapshots with window size at 720x480 (DVD fullscreen) yields 80K snaps on average, with no appreciable slowdown: 80 * 25 * 60 = 120MB @ minute. This seems better than storing bitmaps.
Actual date rate test: demo speed = 975, 18.4 minutes, size 1.256 GB, rate = 73MB @ minute
Random waveform bug can be demonstrated using new OscTest, settings: random, .01, .5, .05, .1
Sunday, August 28, 2005
almost there
During crossfade, random waveform's frequency change glitches horribly, fix this. Also fix a minor glitch when patch is switched without crossfade: in this case clear the view's previous parameter info to prevent interpolation. Ring growth and master speed glitch too but it's less noticeable.
Overall it looks gorgeous and feels incredibly powerful and exciting, it's a different whorld. Master speed of 901 is very nice, maybe use this speed for demo? Maybe a bit too fast, 603 is good too...
Random waveform bug can be demonstrated using new OscTest, settings: random, .01, .5, .05, .1
Overall it looks gorgeous and feels incredibly powerful and exciting, it's a different whorld. Master speed of 901 is very nice, maybe use this speed for demo? Maybe a bit too fast, 603 is good too...
Random waveform bug can be demonstrated using new OscTest, settings: random, .01, .5, .05, .1
Saturday, August 27, 2005
more on clumping
oscillator must move into view so it can be subclocked (non-integer clock increments). See 1.0.69c.
Wednesday, August 24, 2005
clumping
The clumping problem goes all the way back to the beginning. It occurs when master speed and/or ring growth) are big enough to cause multiple rings per timer tick. When the view's timer hook adds multiple rings, the rings all share the same properties, which causes those rings to form an easily distinguishable group or "clump".
The worst offenders are rotation and skew (AKA shift). Both of these are zeroed at birth, and then incremented by the view's draw function. A major reduction of clumping can be obtained simply by offsetting each ring's initial rotation and skew. The offset is a delta multiplied by a fraction. The fraction is calculated by dividing the ring's index by the total number of rings that will be born for this timer tick. If four rings will be born, the fractions for the rings will be 0, .25, .5, and .75 respectively, and if the current rotation delta is .2, the initial rotations for the rings will be 0 * .2, .25 * .2, .5 * .2, and .75 * .2.
The only complication is that the rings are generated in reverse order. This means the indices have to be inverted, e.g. by using (count - 1 - i) instead of i, which means the count must be computed before the ring-adding loop begins. The count can be computed as:
Also note that the resulting offsets must be multiplied by RingGrowth to be effective.
The next-worst offenders are the LFOs. The LFOs are outside the view, but they only modify the view parameters on timer tick boundaries. This means that any LFO has the potential to cause clumping. The high road would move the oscillators into the view but this is a drastic change and has many side effects. The low road is to interpolate between the current and previous parameter values. In practice interpolation seems to work very well for all parameters except ring growth and ring sides. The view timer hook must calculate a delta between each parameter's current and previous values, like so:
And then within the ring-adding loop, overwrite m_Parms with interpolated values:
And finally, restore m_Parms.
Interestingly, this seems to work perfectly with sine LFOs, but exhibits a noticeable periodic discontinuity with triangle LFOs. This seems to indicate an undiscovered problem with the above method, or possibly a bug in the oscillator object.
The final offender is the ring color. This is calculated in the view's timer hook, which means all the rings in a group will have the exact same color. This can probably be solved by interpolation as well.
The worst offenders are rotation and skew (AKA shift). Both of these are zeroed at birth, and then incremented by the view's draw function. A major reduction of clumping can be obtained simply by offsetting each ring's initial rotation and skew. The offset is a delta multiplied by a fraction. The fraction is calculated by dividing the ring's index by the total number of rings that will be born for this timer tick. If four rings will be born, the fractions for the rings will be 0, .25, .5, and .75 respectively, and if the current rotation delta is .2, the initial rotations for the rings will be 0 * .2, .25 * .2, .5 * .2, and .75 * .2.
The only complication is that the rings are generated in reverse order. This means the indices have to be inverted, e.g. by using (count - 1 - i) instead of i, which means the count must be computed before the ring-adding loop begins. The count can be computed as:
int(m_st.RingOffset / m_Parms.RingSpacing) + 1;
Also note that the resulting offsets must be multiplied by RingGrowth to be effective.
The next-worst offenders are the LFOs. The LFOs are outside the view, but they only modify the view parameters on timer tick boundaries. This means that any LFO has the potential to cause clumping. The high road would move the oscillators into the view but this is a drastic change and has many side effects. The low road is to interpolate between the current and previous parameter values. In practice interpolation seems to work very well for all parameters except ring growth and ring sides. The view timer hook must calculate a delta between each parameter's current and previous values, like so:
for (int j = 0; j < 16; j++)
delta[j] = (((double *)&prevparm)[j] - ((double *)&m_Parms)[j]) / count;
And then within the ring-adding loop, overwrite m_Parms with interpolated values:
for (j = 0; j < 16; j++) // NOTE: don't do this to ring growth or # sides
((double *)&m_Parms)[j] = ((double *)&prevparm)[j] + delta[j] * (count - 1 - i);
And finally, restore m_Parms.
Interestingly, this seems to work perfectly with sine LFOs, but exhibits a noticeable periodic discontinuity with triangle LFOs. This seems to indicate an undiscovered problem with the above method, or possibly a bug in the oscillator object.
The final offender is the ring color. This is calculated in the view's timer hook, which means all the rings in a group will have the exact same color. This can probably be solved by interpolation as well.
Subscribe to:
Posts (Atom)