En ^ Ru
AVS Programming Guide
Version 1.1
Tom Young (A.K.A.
PAK-9)
Contents:
Part 0. Introduction
Part 1. The
Fundamentals
Part 2. The Superscope
Part 3. Movements &
Dynamic Movements
Part 4. The Third Dimension
Part 5.
Registers
Part 6. Megabuf and loops
Part 7. Coding Effect
Lists
Part 8. Mouse Input
Part 9. The Triangle APE
Part 10.
Advanced 3D
Part 11. Tips & Tricks
Part 12. Advanced
Exercises
Примечание: Содержимое этого документа являются интеллектуальной
собственностью Thomas Young; Вы можете свободно распространять этот документ, не
изменяя его никаким образом. Этот документ не может включаться в любой
коммерческий продукт для извлечения выгоды или иным способом без специального
разрешения автора. Код содержащийся в этом документе может быть использован
свободно с или без модификации. Advanced Visualization Studio защищена авторским
правом Nullsoft.
Introduction
Добро пожаловать в Программное руководство AVS, документ написанный для
начинающих AVS-художников, чтобы помочь им изучить основы языка программирования
AVS. Этот документ предполагает, что Вы проэкспериментировали, по крайней мере,
несколько раз с эффектами AVS, но при этом никакого предшествующего опыта
программирования не требуется.
Имейте в виду, что это не справочный документ, так это он не заменяет
подсказки по выражениям, которые я рекомендую Вам использовать для ознакомления
с основами синтаксиса и операторов. Это также не математический документ, -
многие люди думают, что комплекс AVS пресетов требует много математики, тем не
менее это случается редко. Скажем, что нам нужно использовать математические
выражения иногда, чтобы решить проблемы программирования, и подготовиться к
некоторой теории.
Формат документа разрабатывается так, чтобы имелась возможность пройти
новые темы в быстром темпе, но если первичные темы кажутся вам слишком
подробными, то можно свободно перейти к следующим частям. Философией подхода
является обучение на собственном опыте, так что большинство частей - краткое
описание с примером или двумя, а также упражнения, которые я рекомендую вам,
чтобы попытаться улучшить понимание темы.
Я не получаю достаточно много отзывов относительно этого руководства, так
что могу только преполагать, что есть нечто неправильное в нём, - если Вы не
согласны с чем-либо или имеете предложения для улучшения документа (любые отзывы
приветствуются), то пишите мне на email: PAK.nine*gmail.com
Философия обучения, которой я рекомендую вам следовать:
Прочитайте
и забудьте
Запишите и запомните
Сделайте и поймите
Part 1: The Fundamentals
Окно AVS является по существу холстом, на котором объекты размещаются и
изменяются для создания эффектов. Основные элементы разделяются на две
категории: Render и Trans элементы. Render элементы буквально рендерятся в окно
так, что пиксели "помещаются" в окне (возможно смешиваются с тем, что там уже
есть). Элементы Trans влияют на существующие пиксели на определенном пути,
сдвигая их или изменяя их значение (запомните, пиксели являются просто красной,
зеленой и синей величиной).
Само окно AVS распределяется от (-1) до (1) по X и Y. Здесь X -
горизонтальное ось, где (-1) является крайним левым, а (1) является крайним
правым размещением. Y - вертикальное ось, где (-1) есть верх, а (1) есть низ.
Следовательно, очевидно, что рендеринг пикселя в (0,0) произведет точку точно в
центре окна AVS.
Программирование в AVS разбито на 4 этапа выполнения (для точек, в которых
выполняется код), Init, Frame, Pixel (или Point) и Beat. Init - основание
инициализации, код в этом боксе выполняется, когда пресет запущен (есть
исключение из этого, но в данный момент примите это как есть). Frame выполняется
один раз для каждого фрейма, когда рендерится AVS. Сумма времени на выполнение
Pixel кода изменяется в зависимости от элемента, например, SuperScope с n
точками выполняется n раз за каждый фрейм. Часто люди делают ошибку, вставляя в
бокс Pixel код, который нужно размещать в боксе Frame, что означает, что код
выполняется многого больше времени чем необходимо, и мы адресуем эту проблему
для всего руководства. Beat код выполняется, когда обнаружен такт в
музыке.
Мы не будем более вдаваться в детали относительно функций, предусмотренных
AVS, поскольку они документированы в помощи выражения; тем не менее, хорошая
идея посмотреть на них для понимания имеющихся возможностей. Вместо этого, Вы
только должны гарантировать, что знакомы со следующим:
* Переменные создаются в назначении, и MyVar=0 создаст переменную
MyVar. Таким образом MyVar=MyVar+1 достаточно, чтобы создать приращение
переменной.
* Назначение сделано с использованием знака (=). Так
MyVar=1 установит величину 1 в переменную MyVar.
* Функции возвращают
значения, поэтому MyVar=sin(2) вызывает функцию sin, и возвращает величину,
которая устанавливается в MyVar. Это относится к большинству функций, хотя
многие просто возвращают 1 или 0.
* Каждое утверждение должно
заканчиваться точкой с запятой. Так MyVar=0;
Всё это - только напоминания, - смотрите в помощи выражения для большего
количества деталей об основном синтаксисе AVS, и некоторые из операторов,
которые доступны.
Part 2: The Superscope
Большинство людей согласилось бы, что SuperScope - главный render объект в
AVS, и это чрезвычайно мощный инструмент. Некоторые важные переменные:
n - Представляет число точек для рендеринга.
b - (beat)
Только для чтения, и имеет значение 1, если есть бит, иначе 0.
i -
Значение которое изменяется от 0 до 1, согласно текущей позиции рендеринга (i
может быть определено как 1/n*p, где p - текущий пункт рендеринга, и, таким
образом, например, 3 точки SuperScope получат значения 0, 0.5 и 1).
v
- Значение формы волны или спектра (как определено) в текущем пункте. Данные
волны или спектра - это сигнал от 0 до 1.
red - Количество красного
цвета, от 0 до 1, может быть прописано в любом боксе.
blue - Аналогично
с синим.
green - Аналогично с зеленым.
linesize - Если эффект
рендерится как линии, то ширина линии, от 0 до 255.
skip - Если
установлено больше ноля, текущий пункт не будет предоставлен.
drawmode
- Если не ноль, будят рендериться линии (Lines), иначе будут рендериться точки
(Dots).
Вы не должны помнить всего, они приведены просто как справка. Эффект
некоторых из этих переменных может фактически копироваться, например v, который
может копироваться из getspec() или getosc().
Достаточно скучной теории, попытаемся получить код. Первый шаблон, который
мы собираемся сделать, является простой областью simple scope, поэтому запустите
AVS, создайте новый пресет и добавьте SuperScope, убедитесь, что "Чистить каждый
кадр" помечен в Main (это очистит окно AVS в начале каждого фрейма). После чего
наберите следующее в кодовых боксах:
Init:
n=800;
Point:
X=2*i-1;
Y=v*0.5;
У нас есть наша область, теперь взглянем на код. Прежде всего мы установили
n в 800 значений, мы хотим 800 пунктов для нашей суперобласти, и если Вы
измените значение на 8, то заметите, на сколько еще не готова область, когда мы
представляем её с меньшим количеством пунктов. Мы помещаем этот код в Init,
только потому, что хотим указать AVS количество пунктов, - переменная
установлена один раз, и мы не должны больше об этом волноваться.
Мы устанавливаем X в 2*i-1 … почему? Поскольку мы хотим, чтобы область
охватила целое окно, мы не можем просто использовать i, потому как этот диапазон
охватывает только от 0 до 1, а мы нуждаемся в диапазоне от -1 до 1. Таким
образом, мы умножаем переменную на 2 (теперь, она охватывает от 0 до 2), и
вычитаем 1 (теперь от -1 до 1).
Наконец мы устанавливаем Y в v*0.5, и мы могли бы использовать только v, но
это выглядит как "грязное" заполнение целого экрана, таким образом мы умножаем
переменную на 0.5, теперь вместо того, чтобы охватить диапазон от -1 до 1,
область охватывает от -0.5 до 0.5 … намного лучше.
Помните, я говорил, что мы могли бы копировать v? Хорошо, попробуем, и если
мы нуждаемся в данных осциллографа, то будем использовать getosc(), синтаксис -
getosc(band,width,channel), где band - полоса образца (0..1), width - ширина
образца (0..1), и channel - канал (0=center, 1=left, 2=right). Не волнуйтесь,
если это слишком сложно для Вас, важная часть - band, width будет 0.1, и channel
будет 0 (центр).
[getosc returns waveform data centered at 'band', sampled 'width' wide,
return value is (-1..1)]
Замените свой пойнткод:
Point:
X=2*i-1;
Y=getosc(x,0.1,0);
Теперь мы поместили x в функцию getosc(), чтобы получать данные области, но
кое-что является неправильным, band должно иметь значение от 0 до 1, а мы отдаём
величину от -1 до 1. Посмотрите, можете ли Вы придумать решение прежде, чем
продолжите.
Решение состоит в том, чтобы умножить и переместить x таким образом, чтобы
переменная находилась в пределах от 0 до 1, а вопрос в том, как нам
преобразовать -1..1 в 0..1? Хорошо, прежде всего, величина в 2 раза больше,
таким образом x*0.5 - это хорошее начало, но теперь мы имеем -0.5..0.5, и нам
нужно переместить значение к 0.5 ... получаем x*0.5+0.5
Point:
X=2*i-1;
Y=getosc(x*0.5+0.5,0.1,0);
Вы только что сделали свою собственную версию v, примите
поздравления.
Exercises:
Сделайте область, которая является вертикальной вместо горизонтальной.
(Exercise 02A.avs)
Сделайте область, которая охватывает диапазон от -0.5 до
0.5 по оси X. (Exercise 02B.avs)
[line]
Теперь попробуем перейти на другую область, создавая суперобласть, которая
чертит линию из произвольного пункта до другого произвольного пункта. Выглядит
как простая проблема, но мы будем смотреть на некоторые новые функции, чтобы
осуществить это. Прежде всего, мы собираемся убрать i … правильно, i для этой
задачи не нужен. Сделайте новый пресет, добавьте SuperScope и установите n=2,
потому что нам потребуется только 2 пункта, в начале и конце. Теперь помните,
что point code выполнен один раз для каждого пункта, таким образом всё, в чём мы
нуждаемся, является способ определения, какой пункт в настоящее время
рендерится. Введите следующий код:
Init:
n=2;
Frame:
drawmode=1;
CurPoint=0;
Point:
CurPoint=CurPoint+1;
Теперь на каждом фрейме происходит следующее:
CurPoint установлен в 0 (frame code)
CurPoint установлен в 1 (point
code)
Пойнткод выполнен
CurPoint установлен в 2 (point
code)
Пойнткод выполнен
Важно, чтобы Вы ухватили эту концепцию, так как она является ключом к
оперированию с SuperScope, frame code выполнен, затем point code выполнен дважды
(потому что n=2), и мы увеличиваем CurPoint в pixel code, чтобы дать нам способ
идентифицировать то, какая точка рендерится.
Теперь, когда у нас есть способ узнать, какой пункт рендерится, мы можем
определить, где записать рендер с использованием утверждения "если" …
Point:
CurPoint=CurPoint+1;
X=if(equal(CurPoint,1),-0.5,0.5);
Y=0;
Здесь мы говорим, что эти X должен быть установлен в -0.5, если величина
CurPoint=1, иначе установливаем в 0.5. Так, если мы расширим эту идею до немного
более полного решения:
Init:
n=2;
X1=-0.5; Y1=0.25;
X2=0.5;
Y2=-0.25;
Frame:
drawmode=1;
CurPoint=0;
Point:
CurPoint=CurPoint+1;
X=if(equal(CurPoint,1),X1,X2);
Y=if(equal(CurPoint,1),Y1,Y2);
Теперь у нас есть суперобласть, которая будет чертить линию от X1,Y1 к
X2,Y2. Теперь попытаемся сделать это решение немного лучше, мы можем фактически
удалить equal() из каждого утверждения "если", потому что утверждение только
проверяет - является ли первый аргумент 0 или нет, чтобы оценить параметр. Так
как у нас есть переменная, которая является или 1, или 2, мы можем сделать
маленькое урегулирование, чтобы сделать её или 0, или 1, и передать
непосредственно в утверждение "если".
Point:
X=if(CurPoint,X1,X2);
Y=if(CurPoint,Y1,Y2);
CurPoint=CurPoint+1;
Перемещая добавление единицы в последнюю строку, мы получаем Curpoint=0 для
первого пункта и 1 для второго.
Exercises:
Сделайте суперобласть, которая протягивается между 3 произвольными
пунктами. (Exercise 02C.avs)
[square]
Прекрасно, линии это очень хорошо, я уверен, что Вы уже попытались сделать
волну синуса и возможно некоторые другие хитрости, но что относительно "твердых"
объектов? Я уверен, что Вы видели пресеты, где люди производят кубы и другие
геометрические объекты, - попытаемся сделать это самостоятельно. Мы будем
придерживаться 2D, потому что Вы можете применить те же самые принципы в 3D, и
это включено в нижеследующюю главу.
Ключевой принцип "твердой" суперобласти то, что она сделана из линий, и
надо надеяться, что это не слишком большое допущение, но линии - это всё, что
SuperScope может сделать (и, конечно, точки). Таким образом, "хитрость" должна
произвести серию линий, которые так сильно упакованы, что они обеспечивают
появление "твердого" предмета. Давайте, для начала, попробуем "твердый" квадрат,
сделаем новый шаблон с SuperScope и впишем следующее:
Frame:
n=w;
drawmode=1;
switch=0.5;
Point:
switch=-switch;
x=0;
y=switch;
Здесь несколько моментов, которые необходимо отметить, - прежде всего мы
переместили n в frame box и установили его как выражение, а не как константу. w
- это переменная, которая содержит ширину окна AVS, таким образом, мы
устанавливали число пунктов к тому значению. Это количество пунктов может
показаться слишком большим, но чтобы предмет выглядел "твердыми", нам нужно
много линий. Переменная находится в frame box, потому что мы хотим, чтобы w
осталось неизменным, даже если пользователь изменит размеры окна AVS, и если бы
код был в Init, то он не был бы исполнен после рестарта пресета.
Вы можете разобраться, что делает переменная switch? Она изменяется от 0.5
до -0.5 для каждого противоположного пункта. "Хорошо, но где мой твердый
квадрат?!" Я слышу, что Вы вопрошаете, и мы надеемся, Вы сообразили, что в
настоящее время суперобласть отдает много линий от 0.5 до -0.5 и обратно,
поэтому, если мы изменим x на (i-0.5), то что получим? Правильно, прекрасный
"твердый" квадрат!
Но он выглядит немного "мягким", поэтому добавим несколько штрихов:
Point:
switch=-switch;
x=i-0.5; y=switch;
blue=cos(x*2);
red=0; green=0;
Мне не нужно объяснять, что делает функция cos(), но замечу, что мы
передали x как аргумент так, чтобы интенсивность цвета изменилась с осью X.
Попытаемся вместо этого изменять цвет через Y:
Point:
switch=-switch;
x=i-0.5; y=switch;
blue=cos(y*2);
red=0; green=0;
Однако, это вообще не выглядит очень уж хорошо, знаете, почему цвет не
изменяется по оси Y? Мы тянем линии между Y=-0.5 и Y=0.5, и цвет выбран для
каждого пункта, таким образом, мы получаем только colurs blue=cos(-0.5*2) и
cos(0.5*2). Другими словами, получаемый цвет приблизительно равен
blue=0.54.
Одно заключительное примечание по оптимизации, вместо того, чтобы иметь
n=w, попробуем n=w*0.5. Выглядит немного "линейно", но легко выйти из
затруднительного положения, просто изменив linesize на 2. В этом варианте ширина
линий даст компенсацию на промежутки:
Frame:
n=w*0.5;
linesize=2;
drawmode=1;
switch=0.5;
Мы делаем это, потому что пункты суперобласти являются весьма
дорогостоящими с точки зрения частоты кадров (frame rate), и поэтому мы хотим
удержать их количество по минимуму.
Это также подходящая ситуация, чтобы проиллюстрировать переменную skip,
которая позволяет отключать правую сторону твердого квадрата:
Point:
switch=-switch;
x=i-0.5;
y=switch;
skip=above(x,0);
blue=cos(x*2+col); red=0; green=0;
Здесь мы устанавливаем skip=1, когда x больше, чем ноль, используя функцию
above(), попытайтесь сами изменить её на below(), и Вы вероятно сможете
предположить эффект, который будет иметь такая замена. Теперь уберём кусок
квадрата из середины:
Point:
switch=-switch;
x=i-0.5;
y=switch;
skip=band(above(x,-0.25),below(x,0.25));
blue=cos(x*2+col);
red=0; green=0;
Здесь мы используем функцию band(), которая означает, что будет возвращено
1, если x будет больше (-0.25) и меньше (0.25). Помните, мы не могли применить
это же к оси Y, потому что мы можем только пропустить пункты, но не можем
разорвать выстраиваемую линию на части.
Exercises:
Создайте твердый квадрат со штриховкой вдоль Оси Y. (Exercise
02D.avs)
Используйте linesize, чтобы сделать твердый квадрат с 2 пунктами
суперобласти. (Exercise 02E.avs)
[shading]
Твердая суперобласть, которую мы сможем раскрасить по обоим осям, является
нашей следующей задачей, для чего мы должны будем разделить каждую из наших
линий на части.
На сколько частей мы делим каждую линию, является вопросом личного
предпочтения, хотя мы не хотим выбирать слишком большое число, поскольку наша
частота кадров создаст большую нагрузку на процессор.
У нас также должно быть постоянное число пунктов (а не n=w*0.5 или
подобное) так, чтобы мы не столкнулись с проблемами вычисления размеров нашего
твердого предмета. Это может быть сделано с динамическим числом пунктов, но
тогда код будет значительно более сложным.
Как вы, возможно, предположили, мы снова собираемся убрать i, потому что
нуждаемся в немного большем контроле над нашими позициями точки. Прежде, чем мы
начнем, мы должны сделать некоторые вычисления, позволяющими сказать, что мы
собираемся иметь 101 линию, каждая из которых будет разделена на 11 пунктов за
линию (эти значения, возможно, не кажутся очень уж круглыми, но позже вы сможете
увидеть логику), и какое же здесь должно быть значение n? Я предложу Вам
подумать об этом, в то время как мы продолжим выполнение …
Перейдём к коду, создайте новый пресет, уcтановите чистить каждый фрейм и
добавьте Superscope.
Init:
n=800;
Frame:
drawmode=1;
line=-1;
linepos=-1;
Point:
x=line; [x=line*h/w;]
y=linepos;
Это значение n является неправильным в настоящее время, но мы изменим его
позже, а пока, пытается увидеть, почему у нас есть переменные line и linepos.
Переменная line будет вертикальной линией, с которой мы имеем дело, а linepos
будет "куском" линии, которую мы в настоящее время рисуем. Поскольку мы
установили линию и linepos в (-1), не трудно увидеть, что мы собираемся
нарисовать линию от верхнего левого до нижнего правого угла. Теперь добавьте
этот код в дополнение строк point box:
line=if(equal(linepos,1),line+0.02,line);
linepos=if(equal(linepos,1),-1,linepos+0.2);
[image001.gif] "Skip" these solid lines
Мы надеемся, что Вы можете следовать за этим кодом, и мы по существу
добавляем 0.2 к linepos каждый раз, таким образом помещая пункт далее вниз
экрана, пока не достигаем основания экрана, где мы задерживаем его на (-1) так,
чтобы мы могли начать новую линию. Непосредственно переменная line проходит
только когда последняя линия была закончена, то есть, когда linepos=1. Остается
только одна проблема, мы все ещё тянем диагональные соединительные линии как
показано серым цветом на диаграмме выше. Мы должны "пропустить" их, поэтому мы
заменяем код на:
skip=equal(linepos,-1);
line=if(equal(linepos,1),line+0.02,line);
linepos=if(equal(linepos,1),-1,linepos+0.2);
Заметьте, что порядок, в котором мы размещаем эти команды, очень важен;
если бы skip был в конце, то мы пропустили бы неправильный кусок линии. Теперь
мы по существу закончили, но вы вероятно заметили, что ваш твердый объект только
отчасти нарисован, таким образом мы должны высчитать правильное значение n.
Поскольку у нас есть 101 линия и 11 пунктов на линии, мы можем использовать в
нашем коде хорошие круглые числа вроде 0.02, и 0.2 … помните, чтобы нарисовать
10 линий сегмента нужно 11 пунктов. Вы вероятно привыкли к значениям n,
являющимся вопросом предпочтения или результатом небольших догадок, но с
проблемой подобно этой, число пунктов - это уже нетривиальная проблема, и если
мы имеем немного или слишком много точек, то получим наложение или разрывы. В
данном случае, мы можем обоснованно высчитать необходимые пункты просто потому,
что у нас есть хороший чистый алгоритм (даже если я сам действительно так
говорю), это - просто число линий, помноженное на число пунктов линии
101*11=1111.
Теперь давайте добавим небольшую ремарку, где equal(linepos,1) была бы
уместна как отдельная переменная, также мы должны изменить linesize, чтобы
заполнить промежутки, и должны просчитать квадрат по маштабу для лучшего
зрительного восприятия. Окончательная версия похожа на это:
Init:
n=1111;
Frame:
drawmode=1;
linesize=w*0.01;
line=-1; linepos=-1;
Point:
x=line;
y=linepos;
newline=equal(linepos,1);
skip=equal(linepos,-1);
line=if(newline,line+0.02,line);
linepos=if(newline,-1,linepos+0.2);
x=x*0.75; y=y*0.75;
Получилось вполне неплохо и аккуратно. Теперь позволим себе добавить
некоторую штриховку, добавив, к уже записанному в point box, строки:
red=sin(x*2); blue=sin(y+0.5); green=sin(x-0.6)*sin(y-0.5);
Вы можете сказать, что штриховка по оси Y является достаточно блочной, даже
при том, что у нас суперобласть с более чем 1000 пунктов. Другими словами,
высококачественная твердая суперобласть очень дорога с точки зрения fps.
Конечно, вы всегда можете увеличить число фрагментов линии …
Exercises:
Доработайте твердую суперобласть, чтобы иметь 21 пункт на линию (Exercise
02F.avs)
Аппроксимируйте твердую суперобласть с w*0.5 линиями и пунктами
h*0.5 на линию (Exercise 02G.avs)
Part 3: Movements & Dynamic Movements
Movements и Dynamic Movements (DM) являются двумя из самых сильных
инструментов trans, которые существуют в AVS, и мы начнём с рассмотрения
Movement, а затем обратим свой взгляд на DM. Главное различие между Movement и
DM в том, что Вы не можете использовать изменяемые переменные в Movement, - код
составлен один раз, и после этого исполняется для каждого пиксела каждого
фрейма.
Movement использует следующие переменные:
d - (depth) значение "глубины"
каждого пиксела;
r - (rotation) значение "вращения" каждого пиксела;
x -
значение x каждого пиксела;
y - значение y каждого пиксела;
Переменные d и r могут использоваться, только если чекбокс "прямоугольные
координаты" снят, а x и y могут использоваться, только если он отмечен. Так
происходит потому, что у Movement есть по существу 2 способа, чтобы сделать
определенные операции (такие как вращение); Чтобы понять эти различные "способы"
действия, мы должны смотреть на системы координат, и если Вы уже знакомы с
прямоугольными и полярными системами координат, то можете пропустить это краткое
введение.
A brief look at coordinate systems
Вы должны уже быть знакомыми с прямоугольной системой координат, так как
это стандартная система координат AVS, и чтобы установить точку по этому методу,
мы определяем x и y компоненты, от 0 до 1 в каждом случае. Это типичная и, мы
надеемся, очень интуитивно понятная система координат, также Вы могли слышать
как она называется декартовой системой координат. Вам может быть любопытно
относительно того, почему ось Y рассматривает верхнюю часть окна как (-1) и
основания (1), а не наоборот, но на практике это не имеет никакого реального
значения, обозначается как "right handed" система координат (более подробно об
этом в разделе 4), и это - просто проектный выбор, который был принят
создателями AVS.
[image002.gif]
Однако, другая общая система координат - это полярная система координат,
где пункт определен расстоянием от центра (d) и вращением (r). В центре окна
d=0, так как это - центр, в пунктах, определенных кругом, касающимся края окна,
d=1. Значение r колеблется от 0 до определённого угла вращения в радианах (r
увеличения против часовой стрелки и уменьшается по часовой стрелке), где,
например, r=pi - это вращение на 180 градусов. Есть уравнение для преобразования
полярных координат в прямоугольные (и обратно), тем не менее, я не хотел бы
сейчас останавливаться на этом, потому что понимать принципиальную схему
важне.
Здесь приведено несколько примеров соответствия значений в 2 системах
координат, попытайтесь увидеть интуитивно (используйте диаграммы для подсказки),
почему они эквивалентны. Помните, что r=0 - то же самое, что отрицательная ось Y
(другой проектный выбор AVS, как обычно бывает, r=0 - то же самое, что
положительная ось X).
[image011.gif]
Rectangular (Cartesian) .. Polar
x=0; y=0; ..
d=0; r=0;
x=0; y=-1; .. d=1; r=0;
x=0;
y=-0.5; .. d=0.5; r=0;
x=0; y=1; .. d=1;
r=pi;
x=1; y=0; .. d=1; r=-pi/2;
Если вы желаете увидеть это более наглядно в действии, откройте AVS пресет
"Demonstration - Coordinate Systems.avs" в папке AVS programming guide.
Закончим с теорией систем координат, вспомним, что это не математический
документ, так что, если у Вас есть проблема с пониманием этой принципиальной
схемы, то предлагаю поискать в другом месте лучшее описание. Переходим к
кодированию…!
Начнём с нового пресета, чистим каждый фрейм, добавляем SuperScope со
следующим кодом:
Init:
n=5;
Frame:
drawmode=1;
linesize=10;
point=0;
Point:
x=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x=x*0.5;
y=y*0.5;
point=if(equal(point,3),0,point+1);
Надеюсь, Вы видите, что так будет нарисован квадрат, и помните, что trans
элементы только усиливают то, что уже отрендерилось, а самостоятельно Movement
ничего не делает. Теперь добавьте Trans-Movement, выберите (user defined) из
списка и введите следующий код:
User defined code:
d=d*2;
Заметьте, что теперь квадрат уменьшился вдвое. Почему? Хорошо, мы говорим,
что для каждого пиксела (в полярных координатах) мы берем пиксел, который на
удвоенном расстояние от оригинала, таким образом для каждого пиксела, где d=1 мы
берем пиксел из d=2 (конечно нет никаких пикселов в d=2, хотя, таким образом мы
просто берем черный пиксел).
[Фактически изображение зуммируется или смещается. Освобождающееся
пространство заполняется базовым цветом.]
Тот же самый эффект может быть достигнут установкой флажка "прямоугольные
координаты" и входом по:
User defined code:
x=x*2;
y=y*2;
Здесь мы делаем нечто очень подобное предыдущему, но только в прямоугольных
координатах, то есть, для каждого пиксела берем пиксел, который дважды удалён от
оригинала по x и y. По началу всё это может выглядеть несколько противоречиво,
потому как люди часто думают: "Хорошо, если я умножу каждое положение на два, то
оно будет в два раза большим", однако помните, что Вы назначаете новое положение
пиксела, основанное на текущих координатах. Итак, основываясь на том, что я
только что сказал, в каком направление этот код переместит пикселы …?
User defined code:
x=x+0.5;
Ответ - влево, потому что мы назначаем каждому пикселу значение пиксела 0.5
направо от него. Помните, что 0.5 - координатная величина, но не число пикселов,
таким образом x=x+1 переместит всё полностью за экран. "Но я хочу переместить
все пикселы n", говорите вы, хорошо, чтобы переместить постоянное число
пикселов, Вы сначала должны вычислить размер одного пиксела с точки зрения
системы координат, и я предлагаю вам попытаться решить эту проблему
самостоятельно (это является частью одного из заключительных упражнений в
разделе 12).
Теперь уберите чекбокс "прямоугольные координаты" и войдите с:
User defined code:
r=r+$pi*0.25;
Здесь мы поворачиваем экран на 45 градусов, где $pi - константа для пи,
которую мы можем использовать вместо того, чтобы набирать вручную. Почему мы не
можем сказать r=r+45? Поскольку помним, что полярная система координат
использует радианы, а не градусы, если вам нужно преобразовать угол из градусов
в радианы, то как раз умножьте значение на pi/180. И да, 45*pi/180 =
pi*0.25.
[full screen gradient]
Посмотрим ещё на очень полезную (и обычно используемую) особенность
Movements, создавать полноэкранные градиенты. Создайте новую суперобласть:
Init:
n=800;
Frame:
drawmode=1;
linesize=1;
Point:
x=2*i-1; y=0;
red=cos(x*$pi+$pi*0.5);
green=cos(x*$pi); blue=cos(x*$pi-$pi*0.5);
Мы имеем простую
суперобласть линии, которая производит градиент красного, зеленого и синего;
однако мы хотели бы, чтобы суперобласть заполнила весь экран, а не была бы
только линией. Давайте попытаемся думать о проблеме из перспективы перемещения
Movement (то есть, какие координаты мы хотим назначить каждому пикселу,
основываясь на их текущих координатах?). Хорошо, мы хотим, чтобы для каждого
пиксела, значение (x) осталась тем же самым, но чтобы значение (y) каждого
пиксела равнялась пункту на суперобласти линии. Так как суперобласть линии сидит
на y=0, мы можем сказать для каждого пиксела x=x и y=0, но, конечно же, x=x
является лишним, таким образом мы можем указать только y=0. Нужно просто
продолжить, добавить Movement после SuperScope, отметить прямоугольные
координаты и добавить код:
User defined code:
y=0; [y=y*0.00000000000000001;]
Результат достигнут, и этот тип эффекта очень полезен, потому что заполняет
целый экран всего только несколькими линиями кода, и выполняется чрезвычайно
быстро. Как фон к пресету, градиент - это хороший выбор (не используйте плохой
RGB градиент хотя, экспериментируйте, чтобы найти что-то более
изысканное.)
[circular gradient]
Давайте теперь расширим эту идею, сделав круговой градиент, то есть, где
цвет изменяется однородно от центра к краю окна. Надо надеяться, вы понимаете,
что сделать это будет намного проще с использованием полярных координат, но вы
вероятно найдёте это решение более трудным. И вы также уже, возможно,
предположили, что мы собираемся использовать r=0, но давайте разберём проблему
логически.
Если Вы представляете полярную систему координат в своем уме, то значение d
и r различно для каждого пиксела, теперь представьте, что случится, если
значение r константа. В этом случае мы говорим, что значение d может изменяться,
поэтому цвет пиксела можно поменять, основываясь на его расстоянии от начала, но
цвет пиксела будет тем же самым для каждого отдельного значения d. Если мы хотим
получить градиент, используя Movement с r=0, то мы должны создать суперобласть
от d=0 до d=1, при r=0, где это? Хорошо, смотрим снова на диаграмму:
[image012.gif]
Таким образом, тогда должно быть довольно ясно, что мы нуждаемся в
суперобласти в прямоугольных координатах от (0,0) к (0,-1). Никаких проблем,
выберите SuperScope и добавьте следующий код:
Init:
n=800;
Frame:
drawmode=1;
linesize=1;
Point:
y=-i; x=0;
red=cos(y*$pi*2+$pi*0.5+$pi);
green=cos(y*$pi*2+$pi); blue=cos(y*$pi*2-$pi*0.5+$pi);
Затем добавьте наш Movement с кодом:
User defined code:
r=0; [r=r*0.00000000000000001;]
Итого, прекрасный круглый градиент. И это всё, на что мы собираемся
смотреть в Movements, согласен, что основательно углубился в проблематику
прямоугольных/полярных координат, но это действительно важно для понимания
механики Movement и Dynamic Movement.
[dynamic movement]
Далее одно очевидное положение:
Dynamic Movement - это Movement, которое
к тому же является динамическим.
Почему это важно? Поскольку, если Вы можете сделать что-то в Movement, Вы
можете сделать это и в DM, поэтому всё, что мы сейчас раскрыли относительно
Movement, также действительно и здесь. Принципиальное различие в том, что теперь
у нас могут быть переменные, которые изменяются, обозначая путь, по которому
задействованные пикселы также могут изменяться динамически.
Нечто заслуживающее напоминания - что хотя Вы можете использовать такой же
(static Movement) код в Movement и DM, качество в DM будет хуже; это потому, что
DM разбивает экран на блоки для большей эффективности. Каждый блок
приблизителен, а не точно высчитан; однако Вы можете улучшить точность,
увеличивая размер сетки "grid size". И если Вы увеличиваете размер сетки, то
хорошей идеей будет использовать мультипликаторы 2 (multiples of 2), потому что
компьютеры работают быстрее с многократными цепями 2 [16; 32; 64], чем с
произвольными числами.
DM компилирован на аналогичные этапы, как и в SuperScope, на init, frame,
beat и pixel, а это означает, что Вы имеете достаточно большой контроль над
движением пикселов. Начните новый шаблон с SuperScope:
Init:
n=5;
Frame:
drawmode=1;
linesize=10;
point=0;
Point:
x=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x=x*0.5;
y=y*0.5;
point=if(equal(point,3),0,point+1);
Это тот самый наш SuperScope квадрат, теперь добавим Dynamic Movement после
SuperScope, со следующим кодом:
Frame:
move=move+0.01;
Pixel:
x=x+move;
Теперь включите Прямоугольные координаты (rectangular coordinates) и
Обертку (wrap). Заметьте, как квадрат преодолевает экран и "оборачивается"
вокруг экрана, когда квадрат уходит одной из сторон. Также мы можем вращать
изображение, используя переменную r, отключите прямоугольные координаты и
введите:
Frame:
move=move+0.01;
Pixel:
r=r+move;
[alpha blending]
Всё это вполне нам знакомо, потому как мы просто применяем эффекты
аналогичные тем, которые пробовали в Movement, за исключением переменных,
которые изменяются. Попытайтесь поставить флажок Смесь (blend) и заметьте, как
изображение смешивается с оригиналом. Фактически, это очень мощная функция,
поскольку мы можем использовать её для смешивания определенных частей
изображения, используя переменную alpha. Альфа установлена в значения от 0 до 1,
где 1 [и более] есть 100% непрозрачность нового изображения, а 0 [и менее] есть
100% непрозрачность старого изображения. Замените код пиксела этим:
Pixel:
x=x+move;
alpha=sin(move);
Теперь мы исчезаем между квадратом суперобласти, и нашими изменёнными (или
"перемещенными") пикселами. Теперь давайте попробуем это:
Pixel:
x=x+move;
alpha=above(y,0);
Здесь мы говорим, что если y>0, то alpha=1, иначе alpha=0, таким образом
мы раскололи экран на две половины.
[buffer save]
Альфа-смешивание может также использовать буфер, и независимо от того, что
было раньше, начните полностью новый пресет, составленный как:
Main
Misc / Buffer
Save
Render / Clear
screen
Trans / Dynamic Movement
Установите Clear screen в White, отметьте Blend в Dynamic Movement и
поставьте исходный комбобокс (combo box) "Что.." в "Buffer 1". Процедура будет
использовать всё что угодно, находящееся в буфере, как данные, которые будут
смешаны с имеющимся материалом, поэтому ничего не изменяя, мы смешиваем 50%
черного (the buffer) с 50% белого (the clear screen), то есть получаем серый
цвет. Надеемся, вы угадали, что:
Pixel:
alpha=0;
Даст нам белый; а это …
Pixel:
alpha=1;
… даст нам черный. Что относительно:
Pixel:
alpha=d;
Мы знаем, что d изменяется от 0 в
центре к 1 на краях окна, таким образом мы получаем хороший градиент от белого к
черному. Теперь давайте попробуем:
Pixel:
alpha=above(d,0.5);
Как можно было бы предположить, изображение является заполненным кругом
(если вы не видите, почему так, то вернитесь ненадолго к краткому обзору системы
координат), но выглядит оно довольно ужасно, не правда ли? Это часто упоминается
как печально известный "уродливый синдром края" (ugly edge syndrome), связанный
с DM. Так происходит, когда вы имеете очень высокую частоту изменения в цвете,
поскольку DM даёт только приблизительные значения для каждого квадрата решетки
(grid square). Мы можем уменьшить эффект, изменяя наш размер сетки (grid size),
попробуйте 32x32, выглядит немного лучше, но всё ещё довольно неровно.
Попробуйте 256x256, и, безусловно, так намного лучше…, но теперь посмотрите на
ваш счетчик частоты кадров (frame rate counter). Падение в частоте кадров до
такой величины неприемлемо, Вы можете думать, что всё плохо, но помните, что код
в нашем pixel box теперь высчитывается в геометрической прогрессии больше раз.
Для того чтобы проиллюстрировать ситуацию, попробуйте изменить ваш код:
Pixel:
loop(10,
assign(a,sin(rand(100)))
);
alpha=above(d,0.5);
Не волнуйтесь о синтаксисе, полный код loop делает 10 произвольных операций
синуса, но взгляните на вашу частоту кадров теперь. Держу пари на частоту,
меньше 10fps, и это только от добавления 10 вычислений синуса. Мысль, которую я
пытаюсь донести, - нужно держать свой размер сетки как можно меньшим, но это
компромисс между качеством и скоростью. Я предложил бы никогда не использовать
gridsize выше 128x128, и для многих применений, очень низкие значения, такие как
4x4, или 8x8 являются совершенно приемлемыми.
В завершение здесь написаны уравнения для того, чтобы преобразовать
полярные координаты в прямоугольные и наоборот, они написаны как код AVS, а не
как математические уравнения:
Rectangular to
polar:
r=atan2(-x,-y);
d=sqrt(sqr(x)+sqr(y);
Polar to
rectangular:
x=-d*sin(r);
y=-d*cos(r);
Part 4: The Third Dimension
Могу представить, что многие сразу перешли к этому разделу, потому как 3D -
очень популярный метод AVS, с которым у людей часто есть трудности. Я
подчеркивал, что это программный гид и не математический документ, но боюсь нам
придётся углубиться в некоторую математику для этой главы.
[rotating square]
Прежде всего, позвольте узнать, как вращать что-либо в 2D, и уже слышу как
вы говорите "это легко, нужно только использовать r… r=r+1, и всё готово!", -
жаль, но неправильно. Мы здесь должны работать с прямоугольными координатами,
чтобы когда мы перейдём в третье измерение, нам не пришлось столкнулись с
проблемами в выражниях нашей 3D проекции. Ключ к вращению - это матрица
вращения, то есть матрица, в которой мы можем передать x, y, суммарное вращение,
и извлечь переведенные пиксели. Первая матрица вращения:
x=x*cos(rot)-y*sin(rot);
y=x*sin(rot)+y*cos(rot);
Где rot - угол вращения в радианах. Не волнуйтесь о понимании в частности
математики за этим, только примите к сведению, что это способ поворачивать
предметы на плоскости. Теперь реализуем это в пресете, сделаем новый пресет,
чистим каждый фрейм, и добавим SuperScope со следующим:
Init:
n=5;
Frame:
rot=0;
point=0;
Point:
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x1=x1*0.5;
y1=y1*0.5;
x=x1*cos(rot)-y1*sin(rot);
y=x1*sin(rot)+y1*cos(rot);
point=if(equal(point,3),0,point+1);
Не переживайте, если вам кажется, что здесь большое количество кода,
попробуйте просмотреть его шаг за шагом. Первые три строки pixel box определяют
квадрат (если вы не видите как это получается, то вернитесь к части 2), а
следующие две строки просто изменяют его размеры, чтобы он был немного меньше.
Заметьте, что мы теперь используем x1 и y1 вместо непосредственно x и y, и это
не строго необходимо в данном случае, но это хорошая практика, -ссылаться на
переменные, а не пикселы напрямую, как вы увидите позже. Последние две строки -
это непосредственно вращение, где используются промежуточные x1, y1 и переменная
rot, которая определена в frame box. Попытайтесь изменить значение rot на что-то
отличное от 0, а затем попробуйте rot=rot+0.01; Так у нас получился вращающийся
квадрат, что потребовало небольших усилий, но, я уверен, вы справились с
этим.
Теперь попытаемся сделать то же самое в DM, устанавим обратно rot=0 в
SuperScope, для того чтобы сделать его снова простым квадратом, затем добавляем
DM после SuperScope, отмечаем прямоугольные координаты и добавляем следующий
код:
Frame:
rot=rot+0.01;
Point:
x1=x;
y1=y;
x=x1*cos(rot)-y1*sin(rot);
y=x1*sin(rot)+y1*cos(rot);
Отметьте, что в этом случае мы ДЕЙСТВИТЕЛЬНО должны поместить x и y в
переменные, потому что мы влияем на пикселы по мере того как мы обрабатываем их.
Попробуйте:
Point:
x=x*cos(rot)-y*sin(rot);
y=x*sin(rot)+y*cos(rot);
Оцените результат.
Exercises:
Создайте SuperScope квадрат, и определите его вращение в градусах.
(Exercise 04A.avs)
Создайте DM, который изменяет размеры экрана до 50% и
поворачивает на pi/5 радиан. (Exercise 04B.avs)
[3D]
Теперь, когда мы справились с 2D вращением, можем перейти к 3D, а для этого
мы нуждаемся ещё в двух матрицах вращения, чтобы иметь три измерения. Далее
следуют матрицы вращения:
x1=x; y1=y; z1=z;
x2=x1*cos(rotz)-y1*sin(rotz);
y2=x1*sin(rotz)+y1*cos(rotz); z2=z1;
x3=x2*cos(roty)+z2*sin(roty); y3=y2;
z3=-x2*sin(roty)+z2*cos(roty);
x4=x3; y4=y3*cos(rotx)-z3*sin(rotx);
z4=y3*sin(rotx)+z3*cos(rotx);
Кажется, что кода слишком много, но вы можете видеть, как некий принцип,
что это одна наша матрица вращения, только примененная дважды. Заметьте, что
вход для матриц вращения является результатом предыдущего. Хорошо, протяните
свою левую руку так, чтобы ваша ладонь была перпендикулярна вам; поставьте свой
большой палец вверх, ваш указательный палец направляется от вас, ваш [средний]
палец указывает вправо (перпендикулярно вашей ладони), а оставшиеся два пальца
прижмите к вашей ладони. Вы должны сделать жест, как это показано на
рисунке:
[image014.gif]
Thumb (y axis)
- Большой палец вверх (ось y);
Index finger (z axis) - Указательный палец от
себя (ось z);
Fore finger (x axis) - [Средний] палец перпендикулярно
ладони (ось x);
Переменные rotx, roty и rotz - количество вращения вокруг этих осей. Таким
образом, вращение вокруг оси Y (изменение roty), соответствует вращению вашей
ладони относительно большого пальца, указывающего вверх. Вращение вокруг Z
(изменение rotz), соответствует вращению вашей руки относительно указательного
пальца, направленного от вас, и т.д.
Один последний шаг необходим прежде, чем мы сможем осуществить всё это, и
как только мы передали наши значения в матрицы вращения, мы должны
"спроектировать" пикселы на нашу 2D область, что немного похоже на интерпретацию
результата соответственно нашему окну. Проекцией является следующее:
x=x4/(z4+2);
y=y4/(z4+2);
Значение "2" фактически является константой, которая представляет
масштабирование окончательного результата, поэтому значение "1" было бы вполне
приемлемо, но приводило бы к слишком большому изображению (поскольку мы делим
наши окончательные значения x и y на него). Хорошо, достаточно теории, давайте
осуществим уже! Сделайте новый пресет, очистите каждую фрейм, и добавьте
SuperScope с кодом:
Init:
n=5;
Frame:
linesize=2;
drawmode=2;
rotx=rotx+0.01; roty=roty+0.02;
rotz=rotz+0.03;
point=0;
Point:
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
z1=0;
x2=x1*cos(rotz)-y1*sin(rotz);
y2=x1*sin(rotz)+y1*cos(rotz); z2=z1;
x3=x2*cos(roty)+z2*sin(roty); y3=y2;
z3=-x2*sin(roty)+z2*cos(roty);
x4=x3; y4=y3*cos(rotx)-z3*sin(rotx);
z4=y3*sin(rotx)+z3*cos(rotx);
x=x4/(z4+2);
y=y4/(z4+2);
point=if(equal(point,3),0,point+1);
Итак мы имеем наш вращающийся квадрат, не слишком плохой. Но этот код
довольно неэффективный, поэтому мы должны от него отказаться. Заметьте, что это
дополнительная оптимизация, поэтому не беспокойтесь, если вы её не понимаете.
Прежде всего, все те синусы и косинусы не нужны для вычисления каждого пиксела,
по мере того как только наше вращение изменяется каждый фрейм, поэтому мы можем
высчитать значения в pixel box.
Frame:
linesize=2; drawmode=2;
rotx=rotx+0.01; roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx); srotx=sin(rotx);
croty=cos(roty);
sroty=sin(roty);
crotz=cos(rotz);
srotz=sin(rotz);
point=0;
Point:
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
z1=0;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz; z2=z1;
x3=x2*croty+z2*sroty; y3=y2;
z3=-x2*sroty+z2*croty;
x4=x3; y4=y3*crotx-z3*srotx;
z4=y3*srotx+z3*crotx;
x=x4/(z4+2);
y=y4/(z4+2);
point=if(equal(point,3),0,point+1);
Кроме того, деления являются дорогостоящими в плане частоты кадров, поэтому
хорошей идеей будет минимизировать затраты, и, таким образом, мы можем
заменить:
x=x4/(z4+2); y=y4/(z4+2);
на
z5=1/(z4+2); x=x4*z5;
y=y4*z5;
Хотя, здесь больше линий кода, косвенно это фактически более эффективно,
потому что мы удалили деление и заменили умножением. Наконец, резервные
переменные, назначенные как x4=x3, могут быть удалены, и мы можем снова
использовать переменные, такие как x1 вместо того, чтобы создать новые каждый
раз. Выходит намного более опрятно:
Init:
n=5;
Frame:
linesize=2;
drawmode=2;
rotx=rotx+0.01; roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx); srotx=sin(rotx); croty=cos(roty);
sroty=sin(roty); crotz=cos(rotz);
srotz=sin(rotz);
point=0;
Point:
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
z1=0;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty; z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx; z1=y2*srotx+z3*crotx;
z2=1/(z1+2); x=x3*z2;
y=y1*z2;
point=if(equal(point,3),0,point+1);
Возможно, немного тяжелее для чтения, но как только вы схватите основы, то
уже сможете следовать за логикой. Хорошей идеей будет написать свой собственный
код, используя всё приведённое в качестве справки, и как гарантию того, что вы
понимаете изложенные здесь принципы.
[ray trace]
Если же мы хотим произвести трехмерный DM, то процесс немного отличается, -
мы все еще нуждаемся в наши матрицах вращения, но поскольку мы работаем с каждым
пикселом, то нам нужно пройти каждый пиксел в матрицах. Так, вместо того, чтобы
определить форму с X1, Y1 и Z1, мы проходим в x и y текущего пиксела, с
константой для z. Создайте новый пресет, очистите каждый фрейм, добавьте Render
Picture (подойдёт любое изображение), и добавьте Trans Dynamic Movement.
Установите в DM Прямоугольные координаты и Обертка, затем введите
следующее:
Frame:
ox=0; oy=0; oz=-1;
rotx=rotx+0.01; roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx); srotx=sin(rotx); croty=cos(roty);
sroty=sin(roty); crotz=cos(rotz); srotz=sin(rotz);
Pixel:
x1=x;
y1=y; z1=1;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx; z1=y2*srotx+z3*crotx;
Ключевая концепция состоит в том, что когда мы имеем дело с трехмерным DM,
мы изменяем окно к нашей собственной форме, представьте, что окно - это сетка,
мы можем исказить её для того, чтобы сформировать формы и проекции. Вообразите
сетку, на которую вы надавливаете своим пальцем, чтобы сформировать форму, - ваш
палец - это "луч", который говорит сетке, где быть на данном этапе. Это довольно
плохая аналогия для "трассировки луча", которая является только сутью того, что
мы делаем, чтобы определить 3D DM. Три переменные, которые мы добавили, ox, oy и
oz, являются "оригиналом" следа нашего луча, другими словами, положение камеры в
нашем 3D мире. Последняя стадия следа луча отсутствует в нашем DM, потому что мы
не решили чего мы всё же хотим показать, давайте попробуем плоскость, потому что
это легко. Самая простая плоскость определяется посредством выражений:
t=-oz/z1;
x=x3*t-ox;
y=y1*t-oy;
Где t - это параметр трассировки луча, добавьте этот код в конце DM и
посмотрите на результат. Может показаться, что есть две плоскости, но каждая -
фактически призрак реальной плоскости. Заметьте, что oz установлен в (-1),
потому что наша плоскость z=0 и так, если бы oz был 0, то наша камера была бы
врезана в плоскости. Чтобы удалить призрачную плоскость, и удалить искажение,
где плоскость простирается в бесконечность, нам нужно добавить некоторое
затемнение расстояния. Добавьте clear screen и buffer save к вашему пресету
следующим образом:
[image015.gif]
Main
Render /
Picture
Misc / Buffer
Save
Render / Clear
screen
Trans / Dynamic Movement
Измените источник (Source) DM (Что...) на Buffer 1, отметьте Смесь (Blend)
и добавьте следующий код к концу вашего pixel box:
alpha=z1;
DM должен теперь выглядеть намного лучше. Мы смешали DM с Clear screen,
чтобы произвести впечатление дальнего тумана; изображение сохранено в буфер,
затем экран очищен, затем DM рендерит плоскость, но смешивает всё по норме z1.
Поскольку z1 - это последнее значение z, которое мы получаем от наших матриц
вращения, оно представляет дистанцию далеко от камеры. Протяните свою руку и
укажите тремя пальцами так, как я показал вам прежде, теперь удерживайте
указательный палец от себя и поворачивайте ваше тело, ваш указательный палец
представляет окончательное значение z1.
Итак, как вы создадите другие формы? Хорошо, вы должны сесть с карандашом и
бумагой, получить математическое уравнение для формы, которую вы хотите
трассировать, и решить это уравнение, чтобы найти ваш параметр трассировки (t).
Понимаю, что это не большая помощь, но вам будет нужно поискать в другом месте
более подробное руководство по вычерчивания луча.
Exercises:
Сделайте 3D SuperScope заштрихованный квадрат. (Exercise
04C.avs)
Сделайте DM плоскость, где x=0. (Exercise 04D.avs)
Part 5: Registers
Иногда необходимо иметь глобальную переменную, которая может быть прочитана
любым элементом AVS пресета. Для этого мы используем регистры, ряд переменных от
reg00 до reg99, которые обрабатываем также как стандартные переменные.
Давайте начнем новый пресет, очистим каждый фрейм и добавим Render / Texer
II. Texer2 очень подобен точечной суперобласти, за исключением того, что мы
можем выбрать изображение для показа вместо многоточия. Введите следующее:
Init:
n=1;
Frame:
pos1=pos1+speed1;
pos2=pos2+speed2;
Xpos=sin(pos1);
Ypos=sin(pos2);
Beat:
speed1=rand(10)/100;
speed2=rand(10)/100;
Point:
X=Xpos; Y=Ypos;
Теперь мы имеем шарик с довольно неустойчивым движением, главным образом
потому, что мы изменяем скорость на каждом бите. Теперь, если мы хотим сделать
SuperScope, которая рисует вертикальную линию через шарик, то мы должны будем
использовать регистры, чтобы сохранить x позицию шарика. Измените frame
code:
Frame:
..
Reg00=Xpos;
Теперь, если мы откроем окно отладки Настройки > Отладка (Settings >
Debug Window) и посмотрим на регистр 0, то увидим, что он обновляет положение X
нашей точки. Теперь добавляем SuperScope и вводим следующий код:
Init:
n=2;
Frame:
Xpos=reg00;
Drawmode=2;
Point:
X=Xpos;
Y=2*i-1;
Там мы без проблем имеем синхронизированный Texer 2 и SuperScope. Обычно
использование регистров требует наличия control scope (области контроля),
являющейся суперобластью, которая ничего не отдает, но вместо этого содержит
код, влияющий на пресет. Например, это может быть предварительное вычисление
синусов и косинусов вращений для трехмерного пресета и хранение их в
регистрах.
rotx=rotx+0.01; roty=roty+0.02; rotz=rotz+0.03;
crotx=cos(rotx);
srotx=sin(rotx);
croty=cos(roty); sroty=sin(roty);
crotz=cos(rotz);
srotz=sin(rotz);
reg01=crotx; reg02=srotx; reg03=croty; reg04=sroty;
reg05=crotz; reg06=srotz;
Это позволяет вычислять синусы и косинусы только один раз за фрейм.
Exercises:
Сделайте control scope для 3D квадрата. (Exercise 05A.avs)
Part 6: Megabuf and loops
Megabuf - массив из миллиона элементов, которые позволяют хранить большие
объемы данных и, что важно, обрабатывать их с циклами. К сожалению, синтаксис
мегабуфера немного отличается от стандартного AVS:
Retrieving data:
MyVar=megabuf(index);
Entering data:
assign(megabuf(index),MyVar);
Где index - пункт мегабуфера (1 к 1 миллиону), чтобы получить данные или же
ввести внутрь. Теперь также будем смотреть на циклы, потому что мегабуфер
довольно бессмысленен, если циклы не используются. Синтаксис loops:
loop(count, statement)
Однако, потому что мы, вероятно, хотим выполнить больше чем одно выражение
в цикле, мы также должны использовать:
exec2(parm1, parm2)
Эта функция оценивает сначала parm1, затем parm2, и возвращает значение
parm2. Может показаться, что здесь слишком много новых функций, чтобы понять всё
за один раз, но, к сожалению, все они необходимы. Давайте будем идти через
пример, чтобы понять, как все они совмещаются…
Мы собираемся сделать SuperScope со 100 пунктами, которые хранят значение
спектра на один этап в течение долгого времени, так, чтобы у нас могла быть
область, которая прокручивается слева направо. Первая вещь, которую мы должны
сделать, - это поместить значение спектра в megabuf(1), что мы делаем подобно
этому:
assign(megabuf(1),-getspec(0.1,0.1,0));
Теперь мы должны создать циклы через 100 элементов мегабуфера и заполнить
каждый предыдущим элементом, то есть, megabuf(i)=megabuf(i-1). Мы должны
образовать циклы от 100-го до 2-го элемента, а не наоборот, иначе мы будем
перезаписывать данные в процессе счёта. Вот код:
index=101;
loop(99,
exec2(
assign(index,index-1),
assign(megabuf(index),megabuf(index-1))
)
);
Всё выглядит весьма запутанным, но проследуем за этим логически. Сначала мы
определили переменную, названную index, - она будет текущим элементом
мегабуфера, с которым мы работаем, поэтому сначала устанавливаем её в 101. Затем
начинаем нашу петлю и устанавливаем первый аргумент в 99, потому что хотим
выполнить следующие части 99 раз, для элементов от 100 до 2. Мы не должны делать
элемент 1, потому что устанавливаем его к значению спектра. Затем у нас есть
exec2, потому что мы хотим выполнить 2 действия… декрементировать индекс, и
устанавить megabuf item (пункт мегабуфера). Помните, что я сказал, что мы,
вероятно, всегда будем хотеть сделать больше чем одно утверждение в цикле?
Причина этого состоит в том, что мы вообще нуждаемся в способе идентифицировать
текущий элемент, с которым мы работаем, и в этом случае переменная index должна
быть увеличена или уменьшена. К сожалению, мы не можем просто сказать
index=index-1, а должны использовать assign(index,index-1), потому что находимся
внутри функции exec2. Наконец, непосредственно операция
assign(megabuf(index),megabuf(index-1)), которая переносит каждый элемент
мегабуфера вперед на единицу. Теперь мы должны вывести наружу элементы
мегабуфера, поэтому в point box мы помещаем:
Point:
index=index+1;
x=2*i-1;
y=megabuf(index);
Но нам нужно установить index в 0, прежде чем этот код будет выполнен,
таким образом в конце frame box мы добавляем index=0. Окончательный SuperScope
код похож на это:
Init:
n=100;
Frame:
drawmode=1;
assign(megabuf(1),-getspec(0.1,0.1,0));
index=101;
loop(99,
exec2(
assign(index,index-1),
assign(megabuf(index),megabuf(index-1))
)
);
index=0;
Point:
index=index+1;
x=2*i-1;
y=megabuf(index);
Теперь, в заключение, некоторые интересные моменты. Также есть ещё
gmegabuf, то есть глобальный мегабуфер; и единственное различие между ними двумя
- это то, что к gmegabuf можно получить доступ из любого элемента AVS, таким же
образом, как это может регистр reg. Помните, что часто область контроля control
scope используется в, пресете, чтобы сохранить весь сложный код и держать важные
кодовые элементы в одном месте. Часто бывает более оправданно использовать
gmegabuf для сложных операций и поместить код в область контроля для свободного
доступа. Можно было бы утверждать, что регистры чрезвычайно избыточны с
глобальным миллионным массивом доступных пунктов, однако регистры вообще делают
для более легкого чтения кода (и при этом не требуется ужасного утверждения
assign).
Exercises:
Сделайте 100-точечный Texer2, который заполняет megabuf 200 случайными
значениями от -1 до 1 и использует каждую пару чисел как x и y для пункта.
(Exercise 06A.avs)
Part 7: Coding Effect Lists
Относительно недавнее дополнение к AVS - способность кодировать списки
эффектов, давая автору способность контролировать активацию списка эффектов и,
до некоторой степени, его вход и выход. Давайте перейдём прямо к примеру,
создадим новый пресет, очистим каждый фрейм, добавим Effect list, снимем отметку
Включить (enabled), отметим Чистить каждый кадр (clear every frame) и
Использовать оценку (use evaluation override). "Использовать оценку" указывает
AVS, что мы хотим поместить код в боксы, и если этого не сделать, то код будет
проигнорирован. Теперь добавьте объект Render / Text [с текстом "Inside effect
list"] в Effect list и поместите "внутрь списка эффектов", перемещая его немного
от центра так, чтобы образовалось некоторое свободное пространство
[слева].
Сейчас список эффекта выключен, так что добавим некоторый код, который
иногда будет включать его, для чего в список эффекта впечатаем следующее:
Frame:
counter=counter+0.1;
enabled=above(sin(counter),0);
Переменная enabled - зарезервированное слово, которое мы используем, чтобы
указать AVS, используем ли мы или нет список эффекта, устанавливая enabled в 0
или 1. Код, который мы вписали, просто производит волну синуса из переменного
счетчика, и включает список эффекта, когда волна синуса больше чем 0. Теперь
замените код:
Frame:
counter=if(equal(beat,1),1,counter-0.05);
enabled=above(counter,0);
Здесь мы устанавливаем счетчик в 1, когда есть beat (beat - другое
зарезервированное слово, которое AVS устанавливает в 1, когда есть удар), иначе
мы уменьшаем его на 0.05. Список эффекта позволен, когда счетчик больше чем 0, а
результирующий эффект состоит в том, что список эффекта позволен на некоторое
время каждый раз, когда есть удар. Теперь мы можем включать и выключать списки
эффекта когда захотим, поэтому давайте переходить к смешиванию (blending). Есть
две важных переменные, когда мы имеем дело с blending effect lists:
[blending]
Alphain - количество входящих данных, которое нужно смешать с
содержимым списка эффекта. Переменная имеет эффект только когда Входное
смешивание (input blending) установлено в Adjustable.
Alphaout - количество содержания списка эффекта, которое нужно
смешать с данными за пределами списка эффекта. Переменная имеет эффект только
когда Выходное смешивание (output blending) установлено в Adjustable.
Эти описания довольно сухи, и трудно представить результаты операций blend,
пока вы немного не поэкспериментируете с ними. Давайте добавим другой текстовый
объект за пределами нашего списка эффекта, таким образом мы сможем понять как
blending работает. Добавьте текстовый объект перед списком эффекта с текстом
"Outside effect list" и опустите немного текст от центра. Установите Выходное
смешивание списка эффекта на Adjustable и замените код списка эффекта:
Frame:
enabled=1;
counter=counter+0.01;
alphaout=abs(sin(counter));
Заметьте, как тексты усиливаются и ослабевают, потому что мы смешиваем
список эффекта in и out. Также отметьте, что в коде у нас есть функция abs(),
которая возвращает абсолютную величину её аргумента (то есть, отрицательные
числа изменены на положительные), что должно гарантировать пребывание alphaout
между 0 и 1.
Теперь попробуйте изменить Входное смешивание на Maximum. Заметьте, что
данные вне списка эффекта всегда видимы, почему так? Потому что, когда список
эффекта не видим, текст будет видим, поскольку он вне списка эффекта; а когда
список эффекта видим, он содержит текст из outside (maximum blended) с текстом
внутри списка эффекта.
Теперь установите Входное в Adjustable, а Выходное на Replace, и введите
следующий код в список эффекта:
Frame:
enabled=1;
counter=counter+0.01;
alphain=abs(sin(counter));
Здесь мы держим данные в списке эффекта видимыми и только усиливаем in и
out данные, потому что наш output установлен на replace, внешний текст
перемешивается с черными пикселами, когда не смешивается в списке эффекта... Вы,
возможно, уже уяснили для себя, что иногда есть больше чем один способ
достигнуть того же самого эффекта, но в более сложных расположениях списков
эффекта, что будет гораздо менее правильным.
Exercises:
Сделайте три списка эффекта, которые смешиваются друг в друга
последовательно, так что 1 смешивается в 2, затем 2 смешивается в 3, затем 3
смешивается в 1 и т.д. (Подсказка: registers) (Exercise 07A.avs)
Part 8: Mouse Input
Использование мыши - мощная особенность AVS, которая позволяет художникам
добавлять интерактивность к их пресетам и даёт возможность использовать меню и
другие окна как функциональные возможности. Мы не заинтересованы разбирать здесь
правильное и неправильное использование для входа мыши (но было бы разумно вам
самим рассмотреть, что даёт пресету контроль за мышью), вместо этого, мы
посмотрим на то, как кодировать mouse input. Главная функция, которая нам
требуется - это Getkbmouse(), и в зависимости от аргумента, который мы передаем
ей, получаем различные значения:
Getkbmouse(1) Returns the X position of the
cursor
Getkbmouse(2) Returns the Y position of the
cursor
Getkbmouse(3) Returns the mouse left button state (0 up, 1
down)
Getkbmouse(4) Returns the mouse right button state (0 up, 1
down)
Getkbmouse(5) Returns the SHIFT state (0 up, 1 down)
[Shift, а не средняя кнопка мыши, как это неправильно заявлено в помощи выражения]
Так, давайте сразу войдём в пример, сделаем новый пресет, очистим каждый
фрейм, и добавим Render / Texer II со следующим кодом:
Init:
N=1;
Point:
x=getkbmouse(1);
y=getkbmouse(2);
Предельно просто, мышь контролирует точку. Но будем честными, точка,
которую вы можете перемещать со своим курсором, не особенно полезна, поэтому
давайте попробуем кое-что, что мы ещё можем использовать… это кнопка! Создайте
новый пресет, очистите каждый фрейм, и добавьте SuperScope со следующим:
//button
Init:
N=5;
Frame:
drawmode=1;
point=1;
Point:
point=(point+1)%4;
x=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x=x*0.4;
y=y*0.2;
Таким способом мы формируем нашу кнопку. Заметьте, что мы используем
оператор модуля (modulus operator), чтобы создать счетчик вместо обычного
утверждения "if", для того чтобы показать, что есть больше чем один способ
приблизиться к большинству проблем. Теперь мы нуждаемся в некотором способе
идентифицировать - была ли кнопка был нажата, поэтому добавьте вторую
суперобласть со следующим кодом:
//button fill
superscope
Init:
N=2;
Frame:
drawmode=1;
linesize=h*0.2;
Point:
point=bnot(point);
x=if(point,-0.4,0.4);
y=0;
red=0.3; blue=0.1; green=0.1;
Здесь мы используем linesize, чтобы сделать твердую суперобласть и
заполнить пространство внутри зоны нашей кнопки, и при этом гарантировать, что
новая область находится перед предыдущей, иначе мы превысим ограничения по линии
кнопки. Теперь, наконец, мы нуждаемся в области контроля, и это всегда хорошая
идея - создать область контроля, имея дело с контролем над мышью, потому что
обычно, входной код мыши связывается с различными частями пресета, и вы не
хотите потерять её следов. Так что, добавьте ещё SuperScope и сотрите все боксы,
чтобы оставить пустую суперобласть, которую мы можем заполнить. Только теперь мы
можем получить код, и первая вещь, которую мы должны сделать, - обнаружение мыши
в области кнопки, так что добавьте следующее к управляющей SuperScope:
//control
scope
Frame:
inbox=
band(
band(above(getkbmouse(1),-0.4),below(getkbmouse(1),0.4)),
band(above(getkbmouse(2),-0.2),below(getkbmouse(2),0.2))
);
Мы здесь несколько раз используем band(), потому что хотим проверить много
условий, которые происходят сразу, поскольку mouse x должна быть выше -0.4 и
ниже 0.4, mouse y… и т.д. Чистый результат всех наших "и" - это то, что наша
переменная inbox будет 1, если все условия будут верны и 0, если любое из
условий ложно. Теперь уже мы можем использовать эту переменную, чтобы произвести
эффект, значит добавляем следующее после вышеуказанного кода:
reg01=inbox;
Если хотите, можете посмотреть в окне отладки (debug) и увидеть, что reg01
изменяется от 0 до 1, когда мышь находится в области, но для намного более
легкого изменения индикации, просто измените код области заполнения объема
кнопки (button fill superscope) на следующее:
//button fill
superscope
Point:
point=bnot(point);
x=if(point,-0.4,0.4);
y=0;
red=0.3+reg01*0.2; blue=0.1; green=0.1;
Здесь мы (косвенно) добавляем значение нашей переменной inbox к цвету,
чтобы получить приятный эффект "mouse over". Единственной вещью, которую
остаётся добавить теперь, является фактический нажим кнопки. Для этого мы
сделаем так, что кнопка изменит цвет когда "нажато", поэтому доработайте область
контроля (control scope) следующим образом:
//control
scope
Init:
Bstate=0;
Frame:
inbox=
band(
band(above(getkbmouse(1),-0.4),below(getkbmouse(1),0.4)),
band(above(getkbmouse(2),-0.2),below(getkbmouse(2),0.2))
);
reg01=inbox;
Pressing=band(getkbmouse(3),inbox);
bstate=if(pressing,bnot(bstate),bstate);
reg02=bstate;
У нас теперь появились две новых переменные, pressing - переменная, которая
будет установлена в 1, когда пользователь нажмет кнопку (то есть, когда левая
кнопка мыши нажата, и мышь находится в области кнопки), и bstate - положение
кнопки, которая или 1, или 0, мы говорим, если кнопка нажата, изменить состояние
на противоположное, так, если она 0, то изменится на 1, и наоборот. Мы сохраняем
положение кнопки в регистре, потому что собираемся использовать его в button
fill superscope, изменяем button fill superscope на:
//button fill
superscope
Point:
point=bnot(point);
x=if(point,-0.4,0.4);
y=0;
red=0.3+reg01*0.2; blue=0.1+reg02; green=0.1;
Теперь кнопка изменит цвет, когда будет нажата. Однако, вы заметили
проблему? Наша кнопка мерцает, когда мы нажимаем ее, и, кажется, изменяется к
случайному положению, можете сказать, почему это? Это из-за следующей линии в
нашей области контроля:
bstate=if(pressing,bnot(bstate),bstate);
Поскольку код выполняется каждый кадр, он каждый кадр переключает bstate на
bnot(bstate), пока мы удерживаем левую кнопку нажатой мыши в области. Нам нужен
некоторый способ делать изменение положения только изменением один раз за
нажатие клавиши мыши, и есть различные способы достигнуть этого, изменяем код
области контроля на следующее:
//control
scope
Frame:
inbox=
band(
band(above(getkbmouse(1),-0.4),below(getkbmouse(1),0.4)),
band(above(getkbmouse(2),-0.2),below(getkbmouse(2),0.2))
);
reg01=inbox;
Pressing=band(getkbmouse(3),inbox);
bstate=if(band(pressing,unlocked),bnot(bstate),bstate);
reg02=bstate;
unlocked=bnot(getkbmouse(3));
Новые линии выделены красным цветом, всё что мы сделали, ввели переменную,
которая установлена в конце кадра, и равняется 1, если мышь отпущена, и 0, если
мышь нажата. Теперь мы изменяем наше состояние, только если pressed и unlocked
равны 1. Если вам сложно понять, как работает переменная unlocked, последуйте за
кодом от пользователя, не нажимающего кнопку, к пользователю, нажимающему её для
нескольких фреймов, после этого не нажимая её вовсе. Помните, тот факт, что
строка находится в конце кода, очень важен.
Теперь это наша законченная кнопка, и для того, чтобы показать её
потенциальную пользу, давайте добавим список эффекта, который включается и
выключается согласно положению кнопки. Добавьте effect list [после SuperScope],
снимите Включить (enabled), поставьте Чистить каждый кадр (clear every frame),
отметьте Использовать оценку (evaluation override), установите Входное и
Выходное смешивание на Maximum, и добавьте следующий код:
Frame:
Enabled=reg02;
Теперь добавьте текстовый объект в список эффекта с текстом "enabled" и
попробуйте вашу кнопку. Просто или нет? Поскольку мы использовали область
контроля и регистры, мы должны просто сослаться на регистр, для того чтобы
получить положение кнопки. Effect lists и Mouse input - очень сильная
комбинация, экспериментирйте с ними, чтобы увидеть, что вы можете
произвести.
Part 9: The Triangle APE
Triangle APE был создан TomyLobo как вариант рендеринга треугольников в
AVS, и учитывая наше предыдущее обсуждение по solid superscopes, польза от такой
возможности должна быть очевидной. Способ, которым мы отдаем треугольники на
экран, немного различается в SuperScope или Texer2, потому что у нас есть три
координаты, чтобы работать с каждым треугольником; таким образом мы ссылаемся на
x1, y1, x2, y2, x3, y3, чтобы определить углы нашего треугольника. Например,
начните новый preset, очистите каждый фрейм и добавьте Render / Triangle со
следующим кодом:
Init:
n=1;
Triangle:
x1=-0.5; y1=-0.5;
x2=0.5;
y2=-0.5;
x3=0; y3=0.5;
Заметьте, что в ape треугольника, n представляет количество треугольников,
которые мы хотим представить, а не число пунктов. Здесь мы описали простой
треугольник, тем не менее точное написание координат треугольника имеет
ограниченную пользу, поскольку мы вероятно хотим представить большее их
количество. Из-за природы треугольников и определения трех координат, код имеет
тенденцию быть более сложным чем в SuperScopes или Texer2’s и часто требует
циклов. Но не переживайте, мы уже охватили все принципиальные понятия и
сопутствующий синтаксис, и теперь просто должны применить его к более сложной
проблеме.
Давайте попытаемся решить простую проблему: предоставление линий
треугольников. Для большинства пресетов, включающих треугольники
непосредственно, вы можете сесть с ручкой и бумагой и примерно вычислить, где вы
хотите, чтобы треугольники находились, но это - простая проблема, которую мы
просто перескочим...
Нам нужен ряд треугольников, поэтому логично было бы определить один
треугольник, затем его переместить, отрендерить, переместить и т.д. Так что,
давайте определим наш одиночный треугольник, нечто маленькое и смещённое
так, чтобы начать наш ряд, для чего доработайте ваш preset:
Triangle:
x1=-0.9; y1=0.1;
x2=-0.8; y2=0.1;
x3=-0.85;
y3=-0.1;
Итак мы продвигаемся, небольшой треугольник получен, теперь, если мы введём
переменную, которая увеличивается каждый раз, когда проходит через код
треугольника, то сможем добавить её к значениям x, чтобы выполнить наше смещение
shift…
Frame:
shift=0;
Triangle:
x1=-0.9+shift;
y1=0.1;
x2=-0.8+shift; y2=0.1;
x3=-0.85+shift;
y3=-0.1;
shift=shift+0.1;
Здесь мы каждый кадр сбразываем shift на ноль, затем в течение каждого
раза, когда код треугольника выполняется (один раз за triangle), значение shift
увеличивается на 0.1. Теперь измените значение n в боксе init на 18 (18
треугольников), и мы получили наш ряд! И ради небольшой забавы, заставим фигуры
ответить на музыку, изменяя значение y3…
Triangle:
x1=-0.9+shift; y1=0.1;
x2=-0.8+shift;
y2=0.1;
x3=-0.85+shift;
y3=getosc(x1*0.5+0.5,0.1,0);
shift=shift+0.1;
Также мы можем добавить к треугольникам кодирование цвета, однако
воспользуемся red1, blue1, green1 (вам приз, если сможете угадать почему
так):
Triangle:
x1=-0.9+shift; y1=0.1;
x2=-0.8+shift;
y2=0.1;
x3=-0.85+shift;
y3=getosc(x1*0.5+0.5,0.1,0);
shift=shift+0.1;
red1=abs(x1); blue1=1-x1;
green1=0.6;
Part 10: Advanced 3D
Вспомним часть 4, где мы рассмотрели 3D, но это были только основы, теперь
мы собираемся посмотреть на некоторые более сложные элементы работы в третьем
измерении. Это самая продвинутая секция данного программного руководства, и если
вы не чувствуете себя комфортно, занимаясь этими темами, то лучше больше времени
потратить на ознакомление непосредственно с основами; нет никакого смысла
пробираться через все эти трудности, не обучаясь чему-нибудь. Многое в этой
части - теория, которую вы можете найти несколько недостаточной; однако главная
цель здесь состоит в том, чтобы дать вам инструменты, чтобы решить программные
задачи, с которыми вы, вероятно, столкнетесь.
При работе с 3D, крайне важно чтобы вы полность понимали среду, которую вы
создаете, мы уже рассмотрели матрицы и проекции вращения, поэтому вам должно
быть легко разобраться с идеей ваших трёх осей x, y и z, которые определяют ваш
виртуальный мир. Один из самых полезных математически инструментов в нашем
распоряжении - векторы, и вы можете знать или не знать, что такое вектор, но, по
существу, это способ описать положение и направление. Однако в нашем графическом
программировании мы имеем тенденцию меньше интересоваться положением, поэтому
обычно используем векторы как раз для того, чтобы описать направление,
например:
MyVector = [0x,0y,1z]
Есть много описаний для векторов, но здесь я говорю, что X компонент моего
вектора 0, Y компонент 0, и Z компонент 1 (это - математическое описание, а не
код AVS). Если вы представляете 3D пространство, вектор MyVector описывает
направление положительного Z, то есть указывает ось Z вниз.
MyVector = [0x,1y,1z]
Теперь MyVector описывает направление 45 градусов между Y и Z осями.
[image020.png]
Принципиальная схема является довольно простой; теперь, одна вещь, которую
мы могли бы захотеть сделать с вектором, - вычислить его длину. Мы делаем это,
используя теорему Пифагора, с которой вы может быть уже знакомы; не хочу
заниматься детализацией этого, а только приведу уравнение, которое говорит, что
"длина вектора равняется квадратному корню суммы квадратов компонентов":
lengthv=sqrt(sqr(vx)+sqr(vy)+sqr(vz));
Где ваш вектор определен через vx, vy, vz.
Теперь, когда мы производим вычисления с векторами, не плохо бы их
нормализовать, что это означает? Это значит, что x, y и z составляют в целом 1
(вектор длины 1 известен как вектор единицы unit vector), а способ, которым мы
достигаем это, - деление каждого компонента на длину вектора. Так в AVS коде это
было бы:
vx=vx/lengthv; vy=vy/lengthv; vz=vz/lengthv;
Теперь мы немного знаем о векторах, и можем посмотреть на некоторое их
использование; так давайте переходить на нормальные вектора. Что есть
нормальные? Нормальное направление связано с наблюдателем, и если у вас есть
наблюдатель, нормальным для него является вектор, который определяет направление
взгляда. Например, держите свою руку с плоской ладонью, и если ваша ладонь - это
поверхность (такая как треугольник или квадрат), нормальным к вашей ладони
является вектор, который описывает направление от вашей руки вдаль.
Нормальные вектора очень полезны по множеству причин; вообразите, например,
что вы хотите осветить куб, если знаете только местоположение всех поверхностей
на кубе, то, действительно, не имеете достаточно данных для того чтобы
произвести любое пристойное освещение. Если же вы знаете направление каждой
поверхности, то можете сказать, оказывается ли каждая сторона перед светом или
нет.
[solid 3D square]
Давайте сделаем пресет, который отдает твердый трехмерный квадрат и
показывает его нормаль как линию, для чего начните новый preset, и добавьте
SuperScope со следующим кодом:
//control scope
Frame:
rotx=rotx+0.01; roty=roty+0.02;
rotz=rotz+0.03;
reg00=rotx; reg01=roty; reg02=rotz;
Это очень простая область контроля (control scope), которую мы используем,
чтобы сохранить наши значения вращения, мы нуждаемся в этом, потому что эти
значения будут использоваться в больше чем одной суперобласти (вернитесь к части
5, если незнакомы с регистрами). Теперь добавьте вторую суперобласть, чтобы
представить квадрат:
//square
Init:
n=1000;
Frame:
rotx=reg00;
roty=reg01; rotz=reg02;
linesize=2;
drawmode=2;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
switch=0.5;
Point:
switch=-switch;
y1=i-0.5;
x1=switch; z1=0;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;
z1=y2*srotx+z3*crotx;
z2=1/(z1+2); x=x3*z2; y=y1*z2;
У Вас не должно быть никакой проблем c пониманием этого кода, если это не
так, то вернитесь к части 4 и освежите свою память на основах 3D (это фактически
решение exercise 4C и 5A). Вы, возможно, заметили, что матрицы вращения немного
ужаты, но это только для того, чтобы оставить свободное место и сделать код
немного меньше.
Таким образом у нас есть вращающийся квадрат, теперь мы должны определить
нормаль к его поверхности. Поскольку наша поверхность является плоскостью к оси
Z (z1=0), мы фактически уже знаем наше нормальное направление - это z=1 или
z=-1. Почему может быть два значения? Поскольку это только отдельный квадрат, то
он может располагаться или вверх, или вниз, и это, действительно, не имеет
значения, мы только должны принять решение и придерживаться выбранного. Если бы
квадрат был частью объекта, такого как куб, то мы хотели бы, чтобы он
позиционировался от центра куба, и мы назначили бы его нормаль, основываясь на
этом. Так что давайте скажем произвольно, что нормальным направлением к этой
поверхности является z=1, и чтобы показать его как линию, мы должны провести
линию через матрицы вращения, с тем же самым вращением, как и поверхность.
Это потому, что поскольку поверхность вращается, нормаль вращается также, таким
образом мы должны её пересчитывать.
Добавьте новый SuperScope со следующим кодом:
Init:
n=2;
Frame:
linesize=2;
drawmode=2;
rotx=reg00; roty=reg01;
rotz=reg02;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
Point:
y1=0;
x1=0;
z1=0;
x2=x1*crotz-y1*srotz;y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;z1=y2*srotx+z3*crotx;
z2=1/(z1+2);
x=x3*z2; y=y1*z2;
Эта вторая суперобласть пока ничего не производит, но 90% нашей работы
сделано, потому что все, что мы должны - добавить немного кода, который чертит
линию от начала к пункту определенному нормальным, то есть от 0,0,0 до 0,0,1,
замените код:
//normal
Init:
n=2;
Frame:
linesize=2;
drawmode=2;
rotx=reg00; roty=reg01;
rotz=reg02;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
point=0;
Point:
y1=0;
x1=0; z1=point;
point=bnot(point);
x2=x1*crotz-y1*srotz;y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;z1=y2*srotx+z3*crotx;
z2=1/(z1+2);
x=x3*z2; y=y1*z2;
red=1; blue=0; green=0;
Там у нас есть наши поверхность и нормаль. Это было довольно простым
случаем, потому что мы знаем нормальное направление, плюс оно было центризовано
на начало, однако, надеюсь, что теперь вы понимаете принципиальную схему немного
лучше.
[cross product, vector multiplication, векторное произведение, векторное
умножение]
В примере мы только попробовали, мы уже знали нормаль из нашей поверхности,
что было очевидно, и это часто имеет место, если вы делаете простые
геометрические формы; например у куба будут стороны, где нормаль: x=1, x=-1, y=1
и т.д., если вы не будете определять его без сторон ориентированных к оси.
Однако, что вы можете сделать, если как раз не сможете угадать нормальный
стороны? Ответ - довольно изящная формула/уравнение, известная как векторное
произведение. Если вы имеете сторону, то можете определить два вектора, которые
лежат на той стороне и высчитать векторное произведение, для того чтобы получить
нормаль. Векторное произведение определено как:
Cx = v1(y) * v2(z) - v1(z) * v2(y)
Cy = v1(z) * v2(x) - v1(x) * v2(z)
Cz = v1(x) * v2(y) - v1(y) * v2(x)
Так вы сводите векторы v1 и v2, и результатом является вектор Cx, Cy, Cz
(вектор, который перпендикулярен v1 и v2). Как вы определяете два вектора,
которые лежат на стороне? Это, действительно, довольно легко, вы просто берете
два пункта, которые находятся на стороне (вы могли бы также использовать
вершины, так как у вас они уже есть), и вычитаете их, чтобы получить вектор,
который находится на поверхности.
Например, мы имеем треугольник, который является стороной, чью нормаль мы
желаем вычислить с гранями:
x1 y1 z1 (Вершина 1)
x2 y2 z2 (Вершина
2)
x3 y3 z3 (Вершина 3)
(Вершина означает просто угол, на всякий случай, если вы не знали). Здесь
наш треугольник в 3D, чтобы вычислить нормаль, мы делаем 2 вектора из вершины 1
к 2, и из вершины 1 к 3:
v1x=x2-x1; v1y=y2-y1; v1z=z2-z1;
v2x=x3-x1; v2y=y3-y1; v2z=z3-z1;
Таким образом, у нас есть два наших вектора, которые мы нашли, вычитая
определённые из вершин, одну от другой, и мы можем выбрать различные комбинации
вершин, пока закончим тем, что определим два вектора, которые лежат на стороне.
Теперь включаем те векторы в общую формулу векторного произведения (cross
product formula), чтобы найти нормаль:
nx = v1y*v2z - v1z*v2y;
ny = v1z*v2x - v1x*v2z;
nz = v1x*v2y -
v1y*v2x;
Есть наша нормаль, определённая nx, ny, nz; хотя на этой стадии было бы
хорошей идеей нормализовать её, потому что она может быть очень большой или
малой, в зависимости от размера треугольника; хотя, мы уже разбирали это,
поэтому я не буду повторять это здесь. Если хотите видеть это в действии, то
можете загрузить пресет "Demonstration - Arbitrary Normal" в директории AVS
Programming Guide. Пресет создает случайный треугольник и показывает его
нормаль, и вы можете заметить, что начало линии нормали базируется не на
треугольнике; это потому, что, если вы помните, мы интересуемся использованием
нормали для определения направления, но не положения. Вы должны быть в состоянии
прочитать код и следовать за шагами, которые мы только что прошли.
[dot product, scalar multiplication, скалярное произведение, скалярное
умножение]
Заключительной темой, которую мы собираемся исследовать, является скалярное
произведение, формула, которая скажет нам угол между двумя векторами. Мы уже
знаем направление стороны, но в настоящее время мы имеем её выраженние только в
виде вектора; в реальном программном контексте это не очень полезно, потому что
мы почти всегда хотим знать угол между нормалью поверхности и чем-то ещё.
Например, если мы хотим знать, направлена ли поверхность от камеры (так, чтобы
мы могли избежать её рендеринга для скорости счёта), для чего нам необходимо
выяснить угол между нормалью поверхности и вектором от камеры к
поверхности.
Формула для скалярного произведения двух векторов:
Dot = (v1x*v2x+v1y*v2y+v1z*v1z)
Другими словами, умножьте каждый компонент вектора вместе и суммируйте
результат, значение, которое вы получаете в итоге, является косинусом угла между
векторами (умноженный их длинами, но это сейчас для нас не важно). Так, в
терминах неспециалистов, результат есть значение между -1 и 1, где 1 означает,
что они направлены каждый в свою сторону, -1 означает, что они смотрят в одном
направлении, и 0 означает, что они на 90 градусах один к другому (по крайней
мере, это всё о чём мы действительно заботимся). И это уже другая ситуация, где
нормализация векторов важна, иначе результат, который вы получаете, не будет
попадать между -1 и 1.
Вы когда-либо слышали о законе косинусов Ламбера (Lambert's cosine law)?
Вероятно нет, но он только заявляет, что кажущаяся яркость стороны
пропорциональна косинусу угла между нормалью из поверхности и направлением
света. Это удобно, не так ли, - если мы определим свет как вектор (указывающий
путь света), и установим цвет нашей стороны как скалярное произведение
нормального и светового векторов, то сторона будет освещена довольно точно без
любой дополнительной работы с нашей стороны.
Давайте сделаем вращающийся квадрат с некоторым освещением, чтобы
продемонстрировать эту идею, сделайте новый preset и добавить SuperScope со
следующим кодом:
Init:
n=1000;
Frame:
linesize=2; drawmode=2;
rotx=rotx+0.01; roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
switch=0.5;
Point:
switch=-switch;
y1=i-0.5; x1=switch; z1=0;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;
z1=y2*srotx+z3*crotx;
z2=1/(z1+2); x=x3*z2; y=y1*z2;
Здесь простой вращающийся квадрат, и первая вещь, которую мы должны
добавить, чтобы сделать некоторое освещение, - вычислить нормаль, мы уже знаем
как сделать это, так что добавьте следующий код в конце frame box:
//define the normal
nx=0;ny=0;nz=1;
//rotate the normal
x1=nx;
y1=ny; z1=nz;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx; z1=y2*srotx+z3*crotx;
Так мы можем получить доступ к вращающейся нормали как к вектору
(x3,y1,z1), и снова мы определили, что нормаль будет z=1, однако z=-1 также
будет не плохо. Затем мы должны определить наш свет как вектор, и мы определим
его, а после этого нормализуем; добавьте следующий код к frame box:
//define the light
lx=1; ly=0; lz=0;
//normalise
it
length=sqrt(sqr(lx)+sqr(ly)+sqr(lz));
lx=lx/length; ly=ly/length;
lz=lz/length;
Таким образом, это наш свет, вектор, который указывает вдоль положительной
оси X. Теперь, надеюсь, вы видите, что нам действительно не нужно нормализовать
этот вектор, потому что сумма его компонентов уже 1. Причина того, что мы
вставляем код внутрь, состоит в том, чтобы вы могли играть с значениями
светового вектора, например, мы могли бы сделать световую поток под углом 45
градусов между осями X и Y, устанавливая lx и ly на 1, код нормализации
(normalization code) превратит его в единичный вектор (unit vector). Заметьте,
что мы не вращали световой вектор, потому что хотим, чтобы свет был постоянным и
блестел на вращающейся плоскости, если бы мы вращали световой вектор с тем же
самым вращением как и поверхность (как мы это сделали для нормали), то
получилось бы так, будто свет присоединен к поверхности, и, таким образом,
освещение не изменялось бы.
Теперь заключительный этап, скалярное произведение светового вектора с
нормалью поверхности, для того чтобы узнать угол между ними, поэтому добавьте
следующий код к концу frame box:
//dot product the light and the normal
dot=x3*lx+y1*ly+z1*lz;
Это не должно быть ничем слишком удивительным, мы просто включаем нормаль
поверхности и световые векторы в dot product formula, представленную ранее. Так,
а как мы используем это значение? Ответ, мы можем просто назначить насыщенность
цвета суперобласти непосредственно этой переменной, так как dot будет 1, когда
поверхность стоит перед светом и <0, когда она отворачивается от него.
Добавьте следующий код к концу point box:
red=dot; blue=dot; green=dot;
Всё готово. Заметьте, как, кажется, будто поверхность освещена, поскольку
она стоит перед нашим виртуальным источником света, и вы можете играть с
величинами, чтобы видеть влияние, который они имеют, устанавливая свет в 0,0,1
(вектор, указывающий вниз положительной оси Z), тогда появится эффект освещения
в направлении камеры, что поможет вам увидеть этот эффект.
[realistic lighting system]
Хорошо, это ваше введение в освещение; здесь мы увидели один тип простого
освещения, лучшей аналогией для эффекта, который мы создали, является "солнце",
потому что наши стороны освещены однородно, в зависимости от вращения. Другими
словами, местоположение нашей стороны не важно, как будто свет находится очень
далеко, но светит очень ярко. Более реалистическая система освещения определила
бы расстояние между светом и поверхностью, и кое-что, чтобы сделать это, у вас
уже есть, потому что мы рассмотрели подробно то, что относится к содержимому
векторов. Один метод для определения расстояния между светом и поверхностью был
бы:
Определить местоположение света, вектор (a);
Определить
местоположение поверхности, вектор (b);
Вычесть (a) из (b), чтобы
создать вектор (c);
Найти длину (c), используя теорему Пифагора
(Pythagoras).
То значение длины есть расстояние между светом и поверхностью, и вы
использовали бы это значение для того чтобы доработать цвет вашей стороны. Мы не
будем углубляться более в детали об этом хотя, в виду того, что вам решать,
хотите ли вы экспериментировать.
Part 11: Tips & Tricks
Этот раздел содержит некоторые полезные подсказки и уловки кодирования,
которые не относятся ни к какому определенному объекту в AVS, но удобны для
того, чтобы отшлифовать ваши пресеты и разрешить некоторые проблемы.
* Sine Waves [Волны синуса]
Синусоидальные эффекты очень полезны в AVS; синусоидальное движение приятно
человеческому глазу и регулярно происходит в природе. Важно понять основы sin и
cos, чтобы эффективно их использовать в пресетах, (базовое) уравнение волны
синуса является следующим:
y=alfa*sin(2*Pi*f*x)
Это есть амплитуда синуса двух пи на частоту f и параметр x. Чтобы
произвести волну синуса амплитудой 0.5 и частотой 2, мы можем сделать
SuperScope, содержащую:
Point:
x=2*i-1; y=0.5*sin(2*$pi*2*x);
Помните, что ось X в AVS охватывает от -1 до 1, таким образом вы будете
видеть 4 периода, а не 2, как вы могли бы ожидать. Экспериментируйте немного с
волнами синуса и синусоидальным движением так, чтобы вы были в состоянии
запланировать и осуществить эффект, который хотите, вместо того, чтобы
беспорядочно бросать в код sin и cos.
* Aspect ratio correction [Коррекция коэффициента сжатия]
Исправление формата изображения - это запись в пресете, например, для DM
или SuperScope, самым простым методом коррекции изображения [по оси X]
будет:
Frame:
asp=h/w;
Point/Pixel:
x=x*asp;
Где x - ваш окончательный x после перемещения, вращения и т.д. Однако, это
предполагается для окна AVS, которое больше по ширине, чем по высоте, иначе,
вероятно, данные выйдут за края экрана. Вы можете применить более сложные
функции, чтобы создать исправление формата изображения для окна любого
размера:
Point/Pixel:
x=x*if(above(w,h),h/w,1);
y=y*if(above(h,w),w/h,1);
* Frame rate independent Movement [Независимое движение показателя частоты
кадров]
Чтобы избежать слишком быстро перемещения в пресете на быстрых компьютерах,
вы можете ограничить скорость движения объектов, базируя их на времени, а не на
частоте кадров.
Frame:
passed=gettime(0)-last;
MyVar=MyVar+passed;
last=gettime(0);
Где ваше движение основано на MyVar. Для того чтобы иметь различные
элементы, перемещающиеся с различными скоростями, вы можете маштабировать
величину passed, например умножая его случайным коэффициентом, изменяемым на
каждом ударе.
* Inactive effect lists [Бездействующие списки эффекта]
Списки эффекта, которые усиливаются и уменьшаются, или имеют alphaout
изменение любого вида, должны становиться неактивными, как только alphaout
становится достаточно малым, что в итоге благоприятно влияет на частоту
кадров:
Frame:
enabled=above(alpha,0.01);
alphaout=alpha;
* Caching static data [Кэширование статических данных]
Статические данные, например, фон градиента в пресете, могут "кэшироваться"
в буфер в начале пресета, для того чтобы улучшить частоту кадров. Просто
поместите эффекты, которые нужно кэшировать, в список эффекта с сохранением в
буфер (buffer save) в конце, а затем используйте следующий код в списке
эффекта:
Init:
hi=0; wi=0;
Frame:
enabled=bor(1-equal(hi,h),1-equal(wi,w));
hi=h;
wi=w;
Это включит пресет, когда он запускается, и (что еще более важно) когда
окно AVS изменено (чтобы данные могли быть повторно вычислены, что, вероятно,
будет необходимо). Вы можете использовать buffer save с restore, чтобы получить
ваши сохраненные данные на любом этапе.
Part 12: Advanced Exercises
Предполагается, что к этой главой вы уже приобрели довольно основательное
понимание программирования AVS и в состоянии перевести большинство ваших идей в
код. В конце концов, веская причина для изучения языка AVS состоит в том, чтобы
позволить вам лучше реализовывать свои идеи и воспроизводить то, что вы
представляете в своей голове. Один из самых больших навыков, в которых нуждается
любой программист, является решение проблемы, имея проблему, вы должны быть в
состоянии придумать творческое и изящное решение, которое даёт реальное
удовольствие от программирования.
Эта последняя глава содержит некоторые упражнения, которыми вы должны
вплотную заняться, прочитать подсказки, если вдруг столкнётесь с трудностями, но
обязательно сначала попытаться решить их самостоятельно. Также, обычно есть
больше чем одно решение проблемы, поэтому не думайте, что ваше решение должно
соответствовать моему, если оно работает. Мы надеемся, это поощрит вас думать
творчески и развивать свой собственный подход к решению проблем AVS. А когда
находитесь в сомнении… используйте expression help!
Exercises:
Создайте точечную суперобласть, которая производит "твердый" квадрат 10 на
10 пикселов в центре окна AVS. Квадрат должен остаться 10x10, независимо от
размера окна AVS. (Exercise 12A.avs)
Создайте SuperScope на 10,000 пунктов, которая показывает данные
осциллографа (oscilloscope data), но обновляется только один раз в секунду.
(Exercise 12B.avs)
Tips for Exercise 12A:
- Сначала решите, как определить 1 пиксел, это будет связано с высотой и
шириной окна AVS.
- Попытайтесь произвести что-либо фиксированной ширины
прежде, чем займетесь вторым измерением.
- Используйте за основу solid
SuperScope из главы 2, но перестройте его к своим потребностям.
- У
идеального решения будет n=100, ваш вариант?
Tips for Exercise 12B:
* Sample the oscilloscope data using getosc(), divide your loop counter by
10,000 for the band.
- Разбейте проблему на меньшие проблемы… вы можете сделать переменную
равной 1, когда секунда истекла?
- Нужно использовать megabuf, чтобы хранить
значения осциллографа.
- Что является максимальным пределом для цикла (даю
подсказку, это ниже 10,000!)? Если не удаётся использовать один цикл, то
возможно получится использовать два.
- Попробуйте данные осциллографа,
используя getosc(), разделите свой счётчик цикла на 10,000 для полосы.
Перевод: А.Панов.
http://avs.chat.ru
Panow©
En ^ Ru
AVS Programming Guide
Version 1.1
Tom Young
(A.K.A. PAK-9)
Contents:
Introduction
Part 1: The
Fundamentals
Part 2: The Superscope
Part 3: Movements & Dynamic
Movements
Part 4: The Third Dimension
Part 5: Registers
Part 6:
Megabuf and loops
Part 7: Coding Effect Lists
Part 8: Mouse Input
Part
9: The Triangle APE
Part 10: Advanced 3D
Part 11: Tips &
Tricks
Part 12: Advanced Exercises
Note: The contents of this document are the intellectual
property of Thomas Young; you may distribute this document freely provided it is
not modified in any way. This document may not be included in any commercial
product for profit or otherwise without the express permission of the author.
The code contained in this document may be used freely with or without
modification. Advanced Visualization Studio is copyrighted by Nullsoft.
Introduction
Welcome to the AVS programming guide, a document written for
novice AVS artists to help them learn the basics of the AVS programming
language. This document assumes you have experimented at least a little bit with
the effects of AVS but no prior programming experience is necessary. Note that
this is not a reference document, so it is not a replacement for the expression
help, which I recommend you use to familiarize yourself with the basics of
syntax and operators. This is also not a maths document, many people think
complex AVS presets require a lot of maths, however this is rarely the case.
Having said that, we need to use maths sometimes to solve programming problems
so be prepared for a little theory.
The layout of the document is designed to introduce new topics
at a steady pace, but if the early topics seem too basic, feel free to skip to
later parts. The philosophy is learning by doing, so most parts are a brief
description with an example or two, as well as exercises that I recommend you
attempt to improve your understanding of the topic.
I don’t get much feedback about this guide so I can only assume
that there is nothing wrong with it, if you disagree or have any suggestions for
improvement (feedback of all kinds is appreciated) email me at:
PAK.nine@gmail.com
A learning philosophy I recommend you subscribe
to:
Read and forget
Write and remember
Do
and understand
Part 1: The Fundamentals
The AVS window is essentially a canvas onto which objects are
placed and manipulated to produce effects. The main elements are split into two
categories, ‘Render’ elements and ‘Trans’ elements. ‘Render’ elements literally
render to the window, that is, pixels are ‘placed’ in the window (possibly
blended with what is already there). ‘Trans’ elements affect the existing pixels
in a certain way, by shifting them or changing their value (remember, pixels are
simply a red, green and blue value).
The AVS window itself spans from -1 to 1 in the X and Y. The X
is the horizontal axis, -1 being the far left and 1 being the far right. The Y
is the vertical axis, -1 being the top and 1 being the bottom. Therefore it
should be obvious that rendering a pixel at 0,0 will produce a dot in the exact
centre of the AVS window.
The programming in AVS is broken into 4 execution times (points
at which the code is run), ‘Init’, ‘Frame’, ‘Pixel’ (or Point) and ‘Beat’.
‘Init’ stands for initialization, code in this box is executed when the preset
is started (there is an exception to this but for now assume this is the case).
‘Frame’ is executed once for every frame that AVS renders. The amount of times
the ‘Pixel’ code will execute will vary depending on the element, for example a
superscope with ‘n’ points will execute ‘n’ times every frame. Often people make
the mistake of putting code in the ‘Pixel’ box which could go in the ‘Frame’
box, which means the code is executed many more times than necessary, we will
address this problem throughout the guide. ‘Beat’ code is executed when there is
a detected beat in the music.
We will not go into great detail regarding the functions
provided by AVS, as these are documented in the expression help; however it is a
good idea to look at these for a quick idea of what is possible. Instead you
should just ensure you are familiar with the following:
* Variables are created on assignment, that is MyVar=0 will
create the variable MyVar. So MyVar=MyVar+1 is enough to create an incrementing
variable
* Assignment is done using the = sign. So MyVar=1 will put the value
of 1 into the variable MyVar.
* Functions return values, so MyVar=sin(2) is
calling the sin function, and returning a value that is put into MyVar. This
applies to most functions, although many simply return a 1 or 0.
* Every
statement should end with a semicolon. So MyVar=0;
These are just reminders, look in the expression help for
more detail about the basic syntax of AVS and some of the operators that are
available.
Part 2: The Superscope
Most people would agree that the superscope is the main render
object in AVS, and it is extremely powerful. Some important variables are:
n - Represents the number of points to render
b - Can only be read, 1 if there is a beat, otherwise
0
i - A value that changes from 0 to 1 according to the point being
rendered,
‘i’ can be defined as 1/n*p where p is the current
point being rendered.
v - The value of the waveform or spectrum
(as specified) at the current point.
The wave/spec data is a signal from 0 to
1, so for example a 3 point superscope will take data from 0, 0.5 and
1.
red - The amount of red colour, from 0 to 1, this can be set in
any box.
blue - As above with blue
green - As above
with green
linesize- If rendered as lines, the line width, 0 to
255
skip - If set above zero, the current point will not be
rendered
drawmode If non zero, will render lines, otherwise it
will render dots.
Phew, well you don’t need to remember all off those, they are
just there as a reference. The effect of some of these variables can actually be
replicated, for example ‘v’, which can be replicated with getspec() or
getosc().
Okay enough of boring theory, lets get coding. The first thing
we are going to make is a simple scope, so fire up AVS and create a new preset
and add a superscope, ensure clear ever frame is ticked in main (this will clear
the AVS window at the start of every frame). Then type the following into the
code boxes:
Init
n=800
Frame
Beat
Point
X=2*i-1;
Y=v*0.5;
Okay there we have our scope, now lets look at the code. First
of all we have set n to 800 meaning we want 800 points for our superscope, if
you change it to 8 you will notice how much more crude the scope is, because we
are representing the scope with less points. We put this code in the init
because we only want to tell AVS how many points there are once, that variable
is set and we don’t need to worry about it any more.
We then set X to 2*i-1… why? Because we want the scope to span
the whole window, we can’t just use i because it only spans 0 to 1, and we need
-1 to 1. So we multiply it by 2 (now it spans 0 to 2) and subtract 1 (now -1 to
1).
Finally we set Y to v*0.5, we could have used just v, but it
looks a bit messy filling the whole screen, so we multiply it by 0.5, now rather
than spanning -1 to 1 it spans -0.5 to 0.5… much nicer.
Remember I said we could replicate v? Well lets give it a go, we
need the oscilloscope data so we will use getosc(), the syntax is
getosc(band,width,channel), where band is the point to sample from 0 to 1, width
is the width of the sample from 0 to 1 and channel is the channel (left, right
or centre). Don’t worry if that doesn’t mean too much to you, the important part
is the ‘band’, ‘width’ will be 0.1 and channel will be 0 (centre). Replace your
point code with this:
Point
X=2*i-1;
Y=getosc(x,0.1,0);
Now we are feeding x into the getosc() function to retrieve
scope data, but something is wrong, ‘band’ should be a value from 0 to 1 and we
are feeding it a value from -1 to 1. See if you can think of a solution before
you continue.
The solution is to multiply and shift the x to make it fall
within 0 and 1, so how do we convert -1 to 1 to 0 to 1? Well first of all its 2
times too big, so x*0.5 is a good start, but now we have -0.5 to 0.5 so we need
to shift it by 0.5… x*0.5+0.5
Point
X=2*i-1;
Y=getosc(x*0.5+0.5,0.1,0);
Tada! You just made your own version of ‘v’,
congratulations.
Exercises:
Make a scope that is vertical instead of horizontal.
(Exercise 2A)
Make a scope that spans from -0.5 to 0.5 in the X.
(Exercise 2B)
Okay lets move onto a different area, creating a superscope that
draws a line from an arbitrary point to another arbitrary point. Seems like a
simple problem, but we will look at some new functions in order to implement it.
First of all we are going to drop i… that’s right, no i for this problem. Make a
new preset, add a superscope and set n to 2, because we are only going to need 2
points, a start and a finish. Now remember that the ‘point’ code is executed
once for each point, so all we need is a way to determine which point is
currently being rendered. Enter the following code:
Init
n=2
Frame
drawmode=1;
CurPoint=0;
Beat
Point
CurPoint=CurPoint+1;
Now each frame the following happens:
CurPoint is set to 0
(frame code)
CurPoint is set to 1 (point code)
Rest of the ‘Point’ code is
executed
CurPoint is set to 2 (point code)
Rest of the ‘Point’ code is
executed
It is important that you grasp this concept as is it key to the
operation of a superscope, the frame code is executed, then the point code is
executed twice (because n=2) and we are incrementing CurPoint in the pixel code
to give ourselves a way of identifying what point is being rendered.
Okay now that we have a way of knowing what point is being
rendered we can specify where to render using an ‘if’ statement…
Point
CurPoint=CurPoint+1;
X=if(equal(CurPoint,1),-0.5,0.5);
Y=0;
Here we are saying the X should be set to -0.5 if the value of
CurPoint is 1, otherwise set it to 0.5. So if we just extend this idea to a
slightly more complete solution:
Init
n=2;
X1=-0.5;
Y1=0.25;
X2=0.5;
Y2=-0.25;
Frame
drawmode=1;
CurPoint=0;
Beat
Point
CurPoint=CurPoint+1;
X=if(equal(CurPoint,1),X1,X2);
Y=if(equal(CurPoint,1),Y1,Y2);
Now we have a superscope that will draw a line from X1,Y1 to
X2,Y2. Okay now lets make this solution a bit better, we can actually remove
that equal() from each of the if statements because an if statement just checks
if the first argument is a 0 or not in order to evaluate. Since we have a
variable that is either 1 or 2, we can make a small adjustment to make it either
0 or 1 and pass it straight into the if statement.
Point
X=if(CurPoint,X1,X2);
Y=if(CurPoint,Y1,Y2);
CurPoint=CurPoint+1;
By moving the addition to the last line, Curpoint will be 0 for
the first point and 1 for the second point.
Exercises:
Make a superscope that draws between 3 arbitrary
points. (Exercise 2C)
Okay lines are all very well and good, I’m sure by now you’ve
tried making a sine wave and maybe some other tricks by now, but what about
solid objects? I’m sure you have seen presets where people produce cubes and
other geometric objects, well lets have a go ourselves. We’ll stick to 2D
because you can apply the same principles in 3D and it’s covered in a later
chapter.
The key principle to ‘solid’ superscope’s is that they are made
of lines, hopefully this isn’t too much of a let down, but lines is all that a
superscope can do (and dots of course). So the ‘trick’ is to produce a series of
lines that are so tightly packed that they have the appearance of a solid
object. Let’s try a solid square to start off with, make a new preset with a
superscope and enter the following:
Init
Frame
n=w;
drawmode=1;
switch=0.5;
Beat
Point
switch=-switch;
x=0;
y=switch;
Okay there are few things to note, first of all we have moved
‘n’ into the frame box and set it to an expression rather than a constant. ‘w’
is a variable that contains the width of the AVS window, so we have set the
number of points to that value. This may seem like a lot of points but in order
to make out object appear solid we need a lot of lines. It is in the frame box
because we want it to remain w even if the user resizes the AVS window, if the
code was in init it would not be executed after the start of the preset.
Can you work out what the variable ‘switch’ does? It changes
from 0.5 to -0.5 for each alternate point. “Okay, but where’s my solid square!”
I hear you cry, well hopefully you have worked out that currently the superscope
is rendering lots of lines from 0.5 to -0.5 and back again, so if we change the
x to i-0.5 what do we get? That’s right a lovely solid square!
But it looks a bit bland so lets add some shading:
Point
switch=-switch;
x=i-0.5;
y=switch;
blue=cos(x*2);
red=0;
green=0;
I don’t need to explain what the cos() function does, but notice
that we have passed x as an argument so that the colour intensity will change
with the x axis. Lets try changing the colour across the Y instead:
Point
switch=-switch;
x=i-0.5;
y=switch;
blue=cos(y*2);
red=0;
green=0;
Oh dear, that doesn’t look very good at all, do you know why the
colour isn’t changing in the Y axis? Well we are drawing lines between Y=-0.5
and Y=0.5, and the colour is set for each point, so we are only getting the
colurs blue=cos(-0.5*2) and cos(0.5*2). In other words, the colour your getting
is approximately blue=0.54.
One final note on optimizations, rather than have n=w, lets try
n=w*0.5. Looks a bit ‘liney’ doesn’t it, well there is an easy fix, change the
linesize to 2. This way the width of the lines will compensate for the
gaps:
Frame
n=w*0.5;
linesize=2;
drawmode=1;
switch=0.5;
We do this because superscope points are costly in terms of
frame rate so we want to keep them to a minimum.
This solid superscope is also a good situation in which to
illustrate the ‘skip’ variable, lets cut off the right hand side of the solid
square:
Point
switch=-switch;
x=i-0.5;
y=switch;
skip=above(x,0);
blue=cos(x*2+col);
red=0;
green=0;
Here we are setting skip to 1 when x is greater than zero using
the above() function, try changing it to below(), you can probably guess the
effect it will have. Okay now lets cut out a chunk from the middle:
Point
switch=-switch;
x=i-0.5;
y=switch;
skip=band(above(x,-0.25),below(x,0.25));
blue=cos(x*2+col);
red=0;
green=0;
Here we are using the band() function, which means it will
return 1 if x is above -0.25 AND below 0.25. Remember, we could not apply this
to the Y axis because we can only skip points, we cannot chop lines up into
pieces.
Exercises:
Create a solid square with shading along the Y
axis. (Exercise 2D)
Use linesize to make a solid square with
a 2 point superscope. (Exercise 2E)
A solid superscope that we can colour in both axis is our next
challenge, for this we will need to divide each of our lines into pieces.
How many pieces we divide each line into is a matter of personal
preference, although we do not want to pick a number too large or our frame rate
will take a big hit.
We should also have a fixed number of points (rather than
n=w*0.5 or something) so that we don’t encounter problems calculating the
dimensions of our solid object. It can be done with a dynamic number of points,
but the code is significantly more complex.
As you might have guessed, we are going to ditch ‘i’ again,
because we need a little more control over our point positions. Before we start
we need to do some calculations, lets say we are going to have 101 lines, each
one divided into 11 points per line (these values may not seem very round, but
later you will see the logic), what should are ‘n’ value be? I’ll let you think
about it while we get on with the implementation…
Right, onto the coding, create a new preset, clear every frame
and add a Superscope.
Init
n=800;
Frame
drawmode=1;
line=-1;
linepos=-1;
Point
x=line;
y=linepos;
That n value is wrong at the moment but we will change it later,
for now, try to see why we have the variables ‘line’ and ‘linepos’. The ‘line’
will be which vertical line we are dealing with and ‘linepos’ will be the
‘chunk’ of the line we are currently drawing. Because we have set line and
linepos to -1 you should be able to see we are going to draw from the top left
to the bottom right. Now add this code to the bottom of the point box:
line=if(equal(linepos,1),line+0.02,line);
linepos=if(equal(linepos,1),-1,linepos+0.2);
Hopefully you can follow this code, what we are essentially
doing is adding to linepos 0.2 each time, thereby putting a point further down
the screen, until we reach the bottom of the screen where we set it back to -1
so that we can start a new line. The ‘line’ itself only moves along when the
last line has been finished, i.e. linepos =1. Only one problem remains, we are
still drawing the diagonal connecting lines as shown in grey in the diagram
above. We need to ‘skip’ these, so we replace the code with:
skip=equal(linepos,-1);
line=if(equal(linepos,1),line+0.02,line);
linepos=if(equal(linepos,1),-1,linepos+0.2);
Note that the order in which we put these commands is very
important; if the skip was at the end we would be skipping the wrong chunk of
the line. Okay we are essentially finished, but you have probably noticed your
solid object is only partly drawn, so we need to calculate the correct ‘n’
value. Notice that because we have 101 lines and 11 points per line, we can use
nice round numbers like 0.02 and 0.2 in our code… remember, a 10 segment line
needs 11 points to draw. You are probably used to ‘n’ values being a matter of
preference or using a little guesswork, but with a problem like this the number
of points is a non-trivial problem, if we have to few or too many we will get
overlap or under-run. In this particular case we can work out the points needed
fairly simply because we have a nice clean algorithm (even if I do say so myself
;) ) it is simply the number of lines times the number of line points
101*11=1111.
So let’s add a little tidying up, that equal(linepos,1) would be
a bit nicer as a separate variable, we need to change the linesize to fill in
the holes, and we should scale the square to a more visible size. The final
version looks like this:
Init
n=1111;
Frame
drawmode=1;
linesize=w*0.01;
line=-1;
linepos=-1;
Point
x=line;
y=linepos;
newline=equal(linepos,1);
skip=equal(linepos,-1);
line=if(newline,line+0.02,line);
linepos=if(newline,-1,linepos+0.2);
x=x*0.75;
y=y*0.75;
There, nice and tidy. Now lets add some shading to the end of
the point box…
red=sin(x*2);
blue=sin(y+0.5);
green=sin(x-0.6)*sin(y-0.5);
You can tell that the shading in the y axis is pretty blocky,
even though we have a superscope with over 1000 points. In other words, high
quality solid superscope’s are very expensive in terms of fps. Of course you
could always increase the number of line divisions…
Exercises:
Modify the solid superscope to have 21 points per
line (Exercise 2F)
Approximate a solid superscope with w*0.5 lines
and h*0.5 points per line
(Exercise 2G)
Part 3: Movements & Dynamic Movements
Movements and Dynamic Movements (DM’s) are two of the most
powerful trans objects that exist in AVS, we will start by looking at movement
and then extend into DM’s. The main difference between a movement and a DM is
that you cannot use changing variables in a movement, the code is compiled once
and then executed for each pixel each frame.
A movement uses the following variables:
d - This value is the ‘depth’ of each
pixel
r - This value is the rotation of each
pixel
x - The x value of each pixel
y - The y value of
each pixel
The d and r values can only be used if the ‘rectangular
coordinates’ checkbox is unchecked and the x and y can only be used if it is
checked. This is because a movement has essentially 2 modes, to make certain
operations easier (such as rotation); To understand these different ‘modes’ of
operation we need to take a look at coordinate systems, if you are already
familiar with rectangular and polar coordinate systems you can skip this brief
introduction.
A brief look at coordinate systems
You should already be familiar with the rectangular coordinate
system since it is the standard coordinate system of AVS, in order to reference
a point with this method we specify an x and a y component, from 0 to 1 in each
case. This is a typical and hopefully very intuitive coordinate system, you may
also hear it referred to as a Cartesian coordinate system. You may be curious as
to why the y axis considers the top of the window -1 and the bottom 1 rather
than vice versa, well in practical terms it makes no real difference, it is
referred to as a ‘right handed’ coordinate system (more on this in part 4) and
it is simply a design choice that was taken by the makers of AVS.
However another common coordinate system is the polar coordinate
system, where a point is defined by the distance from the origin (d) and the
rotation (r). At the centre of the window, d=0 since that is the origin, at the
points defined by a circle touching the edge of the window, d=1. The value of r
ranges from 0 to defining the rotation in radians (r increases anticlockwise and
decreases clockwise), where r=for example is a 180 degree rotation. There is an
equation to convert polar to rectangular coordinates (and back) however I would
rather not introduce it at this point because understanding the concept is more
important.
Here are a few examples of equivalent values in the two
coordinate systems, try to see intuitively (use the diagrams to help) why they
are equivalent. Remember that r=0 is the same as the negative y axis (another
AVS design choice, r=0 is more commonly the same as the positive x axis)
If you wish to see this in action more closely open the AVS
preset ‘Demonstration – Coordinate Systems’ in the AVS programming guide
folder.
Okay that’s the end of coordinate systems theory, remember this
isn’t a maths document so if you are having trouble understanding this concept I
suggest you look elsewhere for a better description. On to the coding…!
Let’s start with a new preset, clear every frame, add a
superscope with the following code:
Init
n=5
Frame
drawmode=1;
linesize=10;
point=0;
Beat
Point
x=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x=x*0.5;
y=y*0.5;
point=if(equal(point,3),0,point+1);
Hopefully you can see that this will draw a square, remember
trans elements only affect what is already rendered so a movement on its own
will do nothing. Now add a movement, select (user defined) from the list and
enter the following code:
User defined code:
d=d*2;
Notice that the square is now half the size. Why? Well we are
saying that for each pixel (in polar coordinates) we take the pixel which is two
times as far away from the origin, so for each pixel where d=1 we take the pixel
at d=2 (of course there are no pixels at d=2 though so we just take a black
pixel).
The same effect can be achieved by checking the ‘rectangular
coordinates’ box and entering:
User defined code:
x=x*2;
y=y*2;
Here we are doing something very similar except in rectangular
coordinates, i.e. for each pixel, take the pixel that is twice as far away from
the origin in the x and the y. This can be a little counter-intuitive at first
because people often think “Well if I multiply each position by two it will be
twice as big” however remember you are assigning a new pixel position based on
the current coordinates. So based on what I have just said, in which direction
will this code move the pixels…?
User defined code:
x=x+0.5;
The answer is left, because we assign each pixel the value of a
pixel 0.5 to the right of it. Remember that 0.5 is a coordinate value, not a
number of pixels, so x=x+1 will shift everything completely off the screen. “But
I want to move everything ‘n’ pixels” you say, well to move a fixed number of
pixels you first need to calculate the size of one pixel in terms of the
coordinate system, I’ll let you try to solve that problem yourself (its part of
one of the final exercises in part 12).
Now uncheck the ‘rectangular coordinates’ box and enter:
User defined code:
r=r+$pi*0.25;
Here we are rotating the screen by 45 degrees, the value $pi is
a constant for pi that we can use instead of typing it out. Why can’t we say r =
r + 45 ? Because remember the polar coordinate system uses radians rather than
degrees, if you need to convert an angle from degrees to radians, just multiply
the value by pi/180. And yes, 45 * pi/180 = pi*0.25.
Lets look at a very useful (and commonly used) feature of
movements, creating full screen gradients. Create a new superscope:
Init
n=800
Frame
drawmode=1;
linesize=1;
Beat
Point
x=2*i-1;
y=0;
red=cos(x*$pi+$pi*0.5);
green=cos(x*$pi);
blue=cos(x*$pi-$pi*0.5);
There we have a simple line superscope that produces a gradient
of red, green and blue; however we would like it to fill the whole screen rather
than just be a line. Let us try to think about the problem from the perspective
of a movement (i.e. what coordinates do we want to assign each pixel based on
their current coordinates?). Well for each pixel, we want the x value to remain
the same, but we want the y value of each pixel to equal a point on the line
superscope. Since the line superscope is sitting on y=0, we can say for each
pixel x=x and y=0, but of course that x=x is superfluous so we can just say y=0.
That was simple, go ahead and add a movement after the superscope, check
rectangular coordinates and add the code:
User defined code:
y=0;
There we have it, this type of effect is very useful because it
fills the whole screen with just a few lines of code, and it runs extremely
fast. So as a background to a preset, a gradient is a good choice (don’t use a
nasty RGB gradient though, experiment to find something more subtle.)
Let’s expand this idea to make a circular gradient, that is,
where the colour varies uniformly from the centre to the edge of the window.
Hopefully you can see that it will be a lot simpler using polar coordinates, but
you will probably find it harder to envisage the solution. You may have also
guessed already that we are going to use r=0, but let’s step through the problem
logically.
If you imagine the polar coordinate system in your mind, the
value of d and r is different for every pixel, now imagine what happens if the
value of r is a constant. In this case we are saying the value of d can vary, so
the pixel colour can vary based on its distance from the origin, but the pixel
colour will be the same for each separate value of d. If we want a gradient
using movement of r=0, then we need to create a superscope from d=0 to d=1 where
r=0, where is this? Well looking back at the diagram:
So it should be pretty clear then that we need a superscope from
rectangular coordinates 0,0 to 0,-1. No problem, make a superscope and add the
following code:
Init
n=800
Frame
drawmode=1;
linesize=1;
Beat
Point
y=-i;
x=0;
red=cos(y*$pi*2+$pi*0.5+$pi);
green=cos(y*$pi*2+$pi);
blue=cos(y*$pi*2-$pi*0.5+$pi);
Then afterwards add our movement with the code:
User defined code:
y=0;
Hooray, a lovely circular gradient. That’s all we are going to
look at for movements, I realize I labored the rectangular/polar coordinates
issue but it really is important for understanding the mechanics of movements
and DM’s.
Right, it’s state-the-obvious time:
A Dynamic Movement is a movement, except it is dynamic
Why is this important? Because if you can do it in a movement,
you can do it in a DM, therefore everything we just covered regarding movement
is still valid here. The big difference is that now we can have variables which
change, meaning the way in which pixels are affected can change dynamically
too.
Something worth noting is that although you can use the same
(static movement) code in movements and DM’s, the quality will be worse in a DM;
this is because the DM breaks the screen down into blocks for efficiency. Each
block is approximated rather than exactly calculated; however you can improve
the accuracy by increasing the ‘grid size’. If you increase the grid size it is
a good idea to use multiples of 2 because computers are faster at working with
multiples of 2 than arbitrary numbers.
DM’s are compiled in similar stages to a superscope, on init,
frame, beat and pixel, meaning you have a lot of control over the movement of
pixels. Start a new preset with a superscope:
Init
n=5
Frame
drawmode=1;
linesize=10;
point=0;
Beat
Point
x=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x=x*0.5;
y=y*0.5;
point=if(equal(point,3),0,point+1);
This is just our superscope square, now add a Dynamic Movement
after it with the following code:
Init
Frame
move=move+0.01;
Beat
Pixel
x=x+move;
Now turn on rectangular coordinates and wrap. Notice how the
square moves across the screen and ‘wraps’ around the screen when it leaves one
side. We can also rotate an image using the r variable, try un-checking
rectangular coordinates and entering:
Init
Frame
move=move+0.01;
Beat
Pixel
r=r+move;
This should all be fairly familiar because we are simply
applying similar effects as we tried in the movement except with variables that
are changing. Try checking the ‘blend’ box and notice how the image is blended
with the original. This is actually a very powerful function as we can use it to
blend certain parts of the image using the ‘alpha’ variable. The alpha is set to
a value from 0 to 1, 1 being 100% opacity of the new image and 0 being 100%
opacity of the old image. Replace the pixel code with this:
Pixel
x=x+move;
alpha=sin(move);
Now we are fading between the superscope square, and our
modified (or ‘moved’) pixels. Now let’s try this:
Pixel
x=x+move;
alpha=above(y,0);
Here we are saying, if y is greater than zero, alpha is 1
otherwise it is zero, so we have split the screen into halves. Alpha blending
can also use a buffer rather than whatever came before it, start a new preset
arranged like so:
Set the Clear screen to white, check ‘blend’ in the dynamic
movement and set the source combo box to ‘Buffer 1’. What we are doing is using
whatever is in the buffer as the data to be blended with, so unchanged we are
blending 50% black (the buffer) with 50% white (the clear screen), i.e. grey. So
hopefully you have guessed that this:
Pixel
alpha=0;
Will give us white; and this…
Pixel
alpha=1;
…will give us black. What about:
Pixel
alpha=d;
Well, we know that d varies from 0 at the centre to 1 at the
edges of the window, so we get a nice gradient from white to black. Now let’s
try:
Pixel
alpha=above(d,0.5);
Yuck, that’s supposed to be a filled circle (if you don’t see
why go back to ‘a brief look a coordinate systems’), looks pretty horrible
right? This is often referred to as the notorious ‘ugly edge syndrome’
associated with DM’s. It occurs when you have a very high frequency change in
colour, since the DM is only approximating the value of each grid square. We can
reduce the effect by changing our grid size, try 32x32, looks a bit better but
its still pretty jagged. Try 256x256, ah yes that’s much better…now look at your
frame rate counter. A drop in frame rate of that magnitude is unacceptable, you
may think its not that bad, but remember that the code in our pixel box is now
being calculated exponentially more times. To illustrate the point try changing
your pixel code to:
Pixel
loop(10,
assign(a,sin(rand(100)))
);
alpha=above(d,0.5);
Don’t worry about the syntax, all the loop code does is 10
arbitrary sine operations, but look at your frame rate now. I’ll wager its under
10fps, and that’s just from adding 10 sine calculations. The point I am trying
to make is that you have to keep your grid size as small as possible, but it is
a tradeoff between quality and speed. I would suggest you never need a gridsize
above 128x128, and for many applications very low values like 4x4 or 8x8 are
perfectly acceptable.
Finally here are the equations for converting polar to
rectangular coordinates and vice versa, they are written as AVS code rather than
mathematical equations:
Rectangular to
polar:
r=atan2(-x,-y);
d=sqrt(sqr(x)+sqr(y);
Polar to rectangular:
x=-d*sin(r);
y=-d*cos(r);
Part 4: The Third Dimension
I can imagine a lot of people skipping to this section, because
3D is a very popular AVS technique that people often have difficulty with. I’ve
stressed that this is a programming guide and not a maths document but I’m
afraid we will need to delve into some maths for this chapter.
First of all, lets learn how to rotate something in 2D, “that’s
easy” I hear you say “just use r… r = r + 1, all done!” bzzzpt wrong, sorry. We
need to work with rectangular coordinates here so that when we extend into the
third dimension we don’t encounter problems expressing our 3D projection. The
key to rotation is rotation matrices, that is, a matrix to which we can pass our
x, y and rotation amount and retrieve the translated pixels. The first rotation
matrix is:
x=x*cos(rot)-y*sin(rot);
y=x*sin(rot)+y*cos(rot);
Where ‘rot’ is the angle of rotation in radians. Don’t worry
about understanding the maths behind this particularly, just accept that this is
how you rotate things. Okay lets implement this into a preset, make a new
preset, clear every frame and add a superscope with the following:
Init
n=5;
Frame
Rot=0;
Point=0;
Beat
Point
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x1=x1*0.5;
y1=y1*0.5;
x=x1*cos(rot)-y1*sin(rot);
y=x1*sin(rot)+y1*cos(rot);
point=if(equal(point,3),0,point+1);
Okay don’t be put off if it seems like a lot of code, let’s just
look through it a step at a time. The first three lines of the pixel box are
defining a square (if you don’t see how go back to part 2), and the next two
lines simply resize it to be a bit smaller. Notice that we are now using x1 and
y1 instead of just x and y, this isn’t strictly necessary in this case, but it
is good practice to reference variables rather than the pixels themselves as you
will see later on. The last two lines are the rotations themselves, they use x1
and y1 and the variable ‘rot’ that is defined in the frame box. Try changing the
value of rot to something other than 0, now try rot=rot+0.01; There we have our
rotating square, it took a little work but I’m sure you followed it.
Now lets try it in a DM, set rot=0 in that superscope to make it
just a plain square again, then add a DM after it, rectangular coordinates and
add the following code:
Init
Frame
rot=rot+0.01
Beat
Point
x1=x;
y1=y;
x=x1*cos(rot)-y1*sin(rot);
y=x1*sin(rot)+y1*cos(rot);
Note that in this case we DO need to place the x and y into
variables, because we are affecting the pixels as we are processing them.
Try:
Point
x=x*cos(rot)-y*sin(rot);
y=x*sin(rot)+y*cos(rot);
Notice the zany results.
Exercises:
Create a superscope square that allows you to specify its
rotation in degrees.
(Exercise
4A)
Create a DM that resizes the screen to 50% and rotates it by pi/5 radians
(rect cords) (Exercise
4B)
Now that we have mastered rotation in 2D we can move onto 3D,
for this we need two more rotation matrices, so that we have 3. The following
are the rotation matrices:
X1=x;
Y1=y;
Z1=z;
x2=x1*cos(rotz)-y1*sin(rotz);
y2=x1*sin(rotz)+y1*cos(rotz);
z2=z1;
x3=x2*cos(roty)+z2*sin(roty);
y3=y2;
z3=-x2*sin(roty)+z2*cos(roty);
x4=x3;
y4=y3*cos(rotx)-z3*sin(rotx);
z4=y3*sin(rotx)+z3*cos(rotx);
Seems like a lot of code, but you can see how it is the some
principle as our one rotation matrix, just applied twice more. Note that the
input for the rotation matrices is the result of the previous one. Okay, hold
out your left hand so that your palm is perpendicular to you; make your thumb
face up, your index finger face away from you and your forefinger point right
(perpendicular to your palm) and clasp your remaining two fingers to your palm.
You should be making a gesture like this:
The variables rotx, roty and rotz are the amount of rotation
around those axis. So rotation around the y axis (changing variable roty) is
like keeping your thumb pointing up and rotating your hand. Rotating around z
(changing rotz) is like keeping your index finger facing away from you and
rotating your hand etc…
One last step is needed before we can implement this, once we
have passed our values through the rotation matrices we need to ‘project’ the
pixels onto our 2D area, this is a little like interpreting the result to fit
our window. The projection is:
x=x4/(z4+2);
y=y4/(z4+2);
The value 2 is actually a constant that represents the scaling
of the final result, so a value of 1 would be acceptable but would result in the
image being larger (as we are dividing our final x and y values by it). Alright
that’s enough theory, let’s implement already! Make a new preset, clear every
frame and add a superscope with the code:
Init
n=5
Frame
linesize=2;
drawmode=2;
rotx=rotx+0.01;
roty=roty+0.02;
rotz=rotz+0.03;
point=0;
Beat
Point
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
z1=0;
x2=x1*cos(rotz)-y1*sin(rotz);
y2=x1*sin(rotz)+y1*cos(rotz);
z2=z1;
x3=x2*cos(roty)+z2*sin(roty);
y3=y2;
z3=-x2*sin(roty)+z2*cos(roty);
x4=x3;
y4=y3*cos(rotx)-z3*sin(rotx);
z4=y3*sin(rotx)+z3*cos(rotx);
x=x4/(z4+2);
y=y4/(z4+2);
point=if(equal(point,3),0,point+1);
And there we have our rotating square, not too bad. But this
code is rather inefficient, so we’re going to have to tidy it up. Note these
optimizations are optional so don’t worry if you don’t understand them. First of
all those sine’s and cosines do not need to be calculated every pixel, as our
rotation only changes every frame, so we can calculate the values in the pixel
box.
Init
n=5
Frame
linesize=2;
drawmode=2;
rotx=rotx+0.01;
roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx);
srotx=sin(rotx);
croty=cos(roty);
sroty=sin(roty);
crotz=cos(rotz);
srotz=sin(rotz);
point=0;
Beat
Point
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
z1=0;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
z2=z1;
x3=x2*croty+z2*sroty;
y3=y2;
z3=-x2*sroty+z2*croty;
x4=x3;
y4=y3*crotx-z3*srotx;
z4=y3*srotx+z3*crotx;
x=x4/(z4+2);
y=y4/(z4+2);
point=if(equal(point,3),0,point+1);
Also, divisions are costly on the frame rate so it is a good
idea to minimize them, so we can replace
x=x4/(z4+2);
y=y4/(z4+2);
…with…
z5=1/(z4+2);
x=x4*z5
y=y4*z5;
Although this is more lines of code it is actually marginally
more efficient because we have removed a division and replaced it with a
multiplication. Finally, the redundant variable assignments such as x4=x3 can be
removed, and we can re-use variables such as x1 rather than creating a new one
each time. Leaving us with a much tidier:
Init
n=5
Frame
linesize=2;
drawmode=2;
rotx=rotx+0.01;
roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx);
srotx=sin(rotx);
croty=cos(roty);
sroty=sin(roty);
crotz=cos(rotz);
srotz=sin(rotz);
point=0;
Beat
Point
x1=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y1=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
z1=0;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;
z1=y2*srotx+z3*crotx;
z2=1/(z1+2);
x=x3*z2;
y=y1*z2;
point=if(equal(point,3),0,point+1);
A little harder to read perhaps, but once you grasp the basics,
you can follow the logic. It is a good idea to write your own code using this as
a reference in order to ensure you understand the principles involved.
If we want to produce a 3D DM, the process is slightly
different, we still need our rotation matrices but because we are working with
every pixel, we need to pass each pixel into the matrices. So rather than
defining a shape with X1,Y1 and Z1, we pass in the x and y of the current pixel,
and a constant for z. Create a new preset, clear every frame, add a picture (any
picture will do) and a DM. Set the DM to rectangular coordinates and wrap, then
enter the following:
Init
Frame
ox=0;
oy=0;
oz=-1;
rotx=rotx+0.01;
roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx);
srotx=sin(rotx);
croty=cos(roty);
sroty=sin(roty);
crotz=cos(rotz);
srotz=sin(rotz);
Beat
Pixel
x1=x;
y1=y;
z1=1;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;
z1=y2*srotx+z3*crotx;
The key concept when dealing with a 3D DM is that we are
reshaping the window to our own shape, imagine the window is a mesh, we can
distort it in order to form shapes and projections. Imagine a mesh that you push
with your finger to form a shape, your finger is the ‘ray’ that tells the mesh
where to be at a given point. This is a rather poor analogy for ‘ray-tracing’
which is essentially what we are doing to define a 3D DM. The three variables we
have added, ox, oy and oz are the ‘origin’ of our ray trace, in other words the
position of the camera in our 3D world. The last stage of the ray trace is
missing in our DM, because we have not decided what we want to show yet, let’s
try a plane because it is easy. The simplest plane is defined by:
t=-oz/z1;
x=x3*t-ox;
y=y1*t-oy;
Where ‘t’ is what’s know as the ray trace parameter, add this
code to the end of the DM and notice the result. It may appear that there are
two planes but one is actually a ghost of the real plane. Notice that oz is set
to -1 because our plane is z=0 and so if oz were 0 our camera would be embedded
in the plane. In order to remove the ghost plane, and remove the distortion as
the plane extends to infinity, we need to add some distance fogging. Add a clear
screen and a buffer save to your preset like so:
Change your DM ‘source’ to Buffer 1, check ‘blend’ and add the
following code to the end of your pixel box:
alpha=z1;
The DM should now look a lot nicer. What we have done is blended
the DM with a clear screen to give the impression of distance fog; the picture
is saved into the buffer, then the screen is cleared, then the DM renders the
plane but blends out everything at a rate of z1. Because z1 is the last value of
z we get from our rotation matrices, it represents the distance away from the
camera. Hold out your hand and point your three fingers as I showed you before,
now keep your index finger facing away from you and rotate your body, your index
finger is representing the final z1.
So how do you create other shapes? Well you need to sit down
with a pencil and paper, get the mathematical equation for the shape you want to
ray trace and solve it to find your ray trace parameter (t). I realize this
isn’t much help, but you will need to look elsewhere for a more comprehensive
ray tracing guide.
Exercises:
Make a solid square 3D superscope
(Exercise 4C)
Make a DM plane where x=0
(Exercise 4D)
Part 5: Registers
Sometimes it is necessary to have a variable which is global,
that is a variable which can be read by any element of your AVS preset. For this
we use registers, a series of variables from reg00 to reg99 that we treat
exactly like standard variables.
Let’s start a new preset, clear every frame and add a Texer2.
Texer2 is very similar to a dot superscope, except that we can pick the picture
to display instead of just a dot. Enter the following into the Texer2:
Init
n=1;
Frame
pos1=pos1+speed1;
pos2=pos2+speed2;
Xpos=sin(pos1);
Ypos=sin(pos2);
Beat
speed1=rand(10)/100;
speed2=rand(10)/100;
Point
X=Xpos;
Y=Ypos;
Now we have a blob with rather erratic movement, mainly because
we are changing the speed on beat. Now if we want to make a superscope that
draws a vertical line through the blob we will need to use registers, to store
the x position of the blob. Change the frame code of the Texer2 to:
Frame
pos1=pos1+speed1;
pos2=pos2+speed2;
Xpos=sin(pos1);
Ypos=sin(pos2);
Reg00=Xpos;
Now if we open the debug window (Settings > Debug Window…)
and look at register 0 we can see that it is updating with the X position of our
dot. Now if we add a superscope and enter the following code:
Init
n=2;
Frame
Xpos=reg00;
Drawmode=2;
Beat
Point
X=Xpos;
Y=2*i-1;
There we have a synchronized Texer 2 and superscope, easy. A
common use for registers is to have a ‘control scope’, that is a superscope that
does not render anything but instead contains code that affects the preset. For
example pre-calculating the sines and cosines of rotations for 3D presets and
storing them in registers.
rotx=rotx+0.01;
roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx);
srotx=sin(rotx);
croty=cos(roty);
sroty=sin(roty);
crotz=cos(rotz);
srotz=sin(rotz);
reg01=crotx;
reg02=srotx;
reg03=croty;
reg04=sroty;
reg05=crotz;
reg06=srotz;
This way the sine’s and cosines are only calculated once per
frame.
Exercises:
Make a control scope for a 3D
square. (Exercise 5A)
Part 6: Megabuf and loops
The megabuf is a million element array, which allows you to
store large volumes of data and, importantly, process it with loops.
Unfortunately the syntax of the megabuf is a little different to the standard
AVS:
Retrieving data:
MyVar=megabuf(index);
Entering data:
assign(megabuf(index),MyVar);
Where ‘index’ is the megabuf item (1 to 1 million) to retrieve
or enter data into. We will look at loops now as well because the megabuf is
pretty pointless if you don’t use loops. The syntax of loops is:
loop(count, statement)
However because we are likely to want to execute more than one
statement in a loop, we also need to use:
exec2(parm1, parm2)
This evaluates parm1, then parm2, and returns the value of
parm2. This may seem like a lot of new functions to get to grips with all in one
go, but unfortunately they are all necessary. Let’s work through an example to
help understand how they all fit together…
We are going to make a 100 point superscope that stores the
value of the spectrum at one point over time, so that we can have a scope that
scrolls from left to right. The first thing we need to do is put the value of
the spectrum into megabuf(1), which we do like so:
assign(megabuf(1),-getspec(0.1,0.1,0));
Now we need to loop through 100 elements of the megabuf and fill
each one with the previous element, that is, megabuf(i)=megabuf(i-1). We have to
loop from the 100th element to the 2nd rather than vice versa otherwise we will
be overwriting ourselves as we go. Here is the code:
index=101;
loop(99,
exec2(
assign(index,index-1),
assign(megabuf(index),megabuf(index-1))
)
);
Okay don’t panic! It looks confusing but let’s follow it
logically. First we have defined a variable called ‘index’, this will be the
current element of the megabuf we are working with, so to start off we set it to
101. Then we begin our loop, we set the first argument to 99, because we want to
execute the next part 99 times, for elements 100 to 2, we don’t need to do
element 1 because we are setting that to the value of the spectrum. Next we have
an exec2, because we want to perform 2 actions… decrementing the index, and
setting the megabuf item. Remember I said we are always likely to want to do
more than one statement in a loop? The reason for this is that we generally need
a way of identifying the current element we are working with, in this case the
‘index’ variable, which needs to be incremented or decremented. Unfortunately we
can’t just say ‘index=index-1’, we have to use ‘assign(index,index-1)’ because
we are inside an exec2 function. Finally the operation itself
‘assign(megabuf(index),megabuf(index-1))’, this shifts each megabuf element
along by one. Phew! But we’re not finished yet I’m afraid, we need to output the
megabuf elements, so in the point box we put:
Point:
index=index+1;
x=2*i-1;
y=megabuf(index);
But we need to set the index to 0 before this code gets run, so
at the end of the frame box we add index=0. The final superscope code looks like
this:
Init
n=100;
Frame
drawmode=1;
assign(megabuf(1),-getspec(0.1,0.1,0));
index=101;
loop(99,
exec2(
assign(index,index-1),
assign(megabuf(index),megabuf(index-1))
)
);
index=0;
Beat
Point
index=index+1;
x=2*i-1;
y=megabuf(index);
Now for some closing points of interest: There is also a
‘gmegabuf’, that is, a global megabuf; the only difference between the two is
that the gmegabuf can be accessed from any AVS element, in the same way in which
a register can. Remember that often a ‘control scope’ is used in presets to
store all of the complex code and keep important code elements in one place,
frequently it is tidier to use the gmegabuf for complicated operations and place
the code in a control scope for easy access. One might argue that registers are
essentially redundant with a global million item array available, however
registers generally make for easier to read code (and do not require ugly
‘assign’ statements).
Exercises:
Make a 100 point Texer2 that fills the megabuf with 200 random
values from -1 to 1 and uses each pair of numbers as the x and y for a
point. (Exercise
6A)
Part 7: Coding Effect Lists
A relatively recent addition to AVS is the ability to code
effect lists, giving the author the ability to control the activation of an
effect list and (to some extent) its input and output. Let’s jump straight into
an example, create a new preset, clear every frame, add an effect list, uncheck
‘enabled’, check ‘clear every frame’ and check ‘use evaluation override’. ‘Use
evaluation override’ tells AVS that we want to put code into the boxes, if you
do not tick it your code will be ignored. Now add a ‘text’ object inside the
effect list and type “Inside effect list”, move it up a little from the centre
so we have some room for more text.
Now the effect list is currently disabled, so let’s add some
code that will enable it occasionally, type the following into the effect
list:
Frame
counter=counter+0.1;
enabled=above(sin(counter),0);
The variable ‘enabled’ is a reserved word we use to tell AVS
whether or not to enable the effect list, by setting it to 0 or 1. The code we
entered simply produces a sine wave from the variable counter, and enables the
effect list when the sine wave is greater than 0. Now replace the code
with:
Frame
counter=if(equal(beat,1),1,counter-0.05);
enabled=above(counter,0);
Here we are setting the counter to be 1 when there is a beat
(‘beat’ is another reserved word that AVS sets to 1 when there is a beat),
otherwise we reduce it by 0.05. The effect list is enabled when counter is
greater than zero, the net effect is that the effect list is enabled for a
little while each time there is a beat. Right, now we can enable and disable
effect lists as we want, let’s move onto blending. There are two important
variables when dealing with blending effect lists:
Alphain This is the amount of the incoming data to blend
with the contents of the effect list. This variable only has an effect when the
input blending is set to ‘Adjustable’.
Alphaout This is the amount of
the contents of the effect list to blend with the data outside of the effect
list. This variable only has an effect when the output blending is set to
‘Adjustable’.
These descriptions are rather dry, and it is difficult to
envisage the results of the blend operations until you have experimented with
them a bit. Let’s add another text object outside of our effect list so we can
get an idea of how the blending works. Add a text object before the effect list
with the text “Outside effect list” and move it down from the centre a little.
Set the output of the effect list to adjustable and replace the effect list code
with:
Frame
enabled=1;
counter=counter+0.01;
alphaout=abs(sin(counter));
Notice how the texts fade in and out because we are blending the
effect list in and out. Also note in the code we have an abs() function, which
returns the absolute value of its argument (that is, negative numbers are
changed to positive), this is to ensure our alphaout stays between 0 and
1.
Now try changing the input of the effect list to maximum. Notice
that the data outside the effect list is always visible, why is this? It is
because when the effect list is not visible, the text will be visible as it is
outside the effect list; and when the effect list is visible, it contains the
text from outside (maximum blended) with the text inside the effect list.
Okay, now set the input blending to adjustable and the output to
replace and enter the following code into the effect list:
Frame
enabled=1;
counter=counter+0.01;
alphain=abs(sin(counter));
Here we are keeping the data inside the effect list visible and
just fading in and out the data from outside, because our output is set to
‘replace’, the outside text is overwritten with black pixels when not blended
into the effect list.. You may have already spotted that there is sometimes more
than one way to achieve the same effect, but in more complex arrangements of
effect lists, this becomes less and less true.
Exercises:
Make three effect lists which blend into each other
sequentially, such that 1 blends into 2, then 2 blends into 3, then 3 blends
into 1 etc…(Hint: registers)
(Exercise 7A)
Part 8: Mouse Input
Mouse input is a powerful feature of AVS, it allows artists to
add user interactivity to their presets and introduces the possibility of menus
and other window like functionality. We aren’t concerned here with what the
correct and incorrect uses are for mouse input (but you would be wise to
consider what mouse control brings to a preset) instead we will look at how to
code for mouse input. The main function we require is Getkbmouse(), depending on
the argument we pass to it, we will receive a different value:
Getkbmouse(1) Returns the X position of the
cursor
Getkbmouse(2) Returns the Y position of the
cursor
Getkbmouse(3) Returns the mouse left button state (0 up, 1
down)
Getkbmouse(4) Returns the mouse right button state (0 up, 1
down)
Getkbmouse(5) Returns the SHIFT state (0 up, 1 down)
[NOT middle mouse as incorrectly stated in the expression help]
So let’s jump into an example, make a new preset, clear every
frame and add a Texer2 with the following code:
Init
N=1;
Frame
Beat
Point
x=getkbmouse(1);
y=getkbmouse(2);
Gosh, that was simple, a mouse controllable dot. But to be
honest a dot that you can move around with your cursor isn’t particularly
useful, so let’s try something we can use…a button! Create a new preset, clear
every frame, and add a superscope with the following:
Init
N=5;
Frame
drawmode=1;
point=1;
Beat
Point
point=(point+1)%4;
x=-equal(point,0)+equal(point,1)+equal(point,2)-equal(point,3);
y=equal(point,0)+equal(point,1)-equal(point,2)-equal(point,3);
x=x*0.4;
y=y*0.2;
There we have our button. Notice we are using the modulus
operator to create a counter instead of the usual ‘if’ statement, just to show
that there is more than one way to approach most problems. Now we are going to
need some way of identifying if it has been pressed, so add another superscope
with the following code:
Init
//button fill
superscope
N=2;
Frame
drawmode=1;
linesize=h*0.2;
Beat
Point
point=bnot(point);
x=if(point,-0.4,0.4);
y=0;
red=0.3;
blue=0.1;
green=0.1;
Here we are using linesize to make a solid superscope and
filling in our button area, ensure that this scope is before the previous one or
we will overdraw our button outline. Now finally we need a control scope, it is
always a good idea to have a control scope when dealing with mouse control
because mouse input code is usually associated with various parts of the preset
and you don’t want to lose track of it. So add a superscope and erase all of the
boxes to leave an empty superscope we can fill in. Right, now we can get coding,
the first thing we need to do is detect if the mouse is inside the button area,
so add the following to the control superscope:
Frame
inbox=
band(
band(above(getkbmouse(1),-0.4),below(getkbmouse(1),0.4)),
band(above(getkbmouse(2),-0.2),below(getkbmouse(2),0.2))
);
We are using band() a lot here, because we want to check that
lots of conditions are occurring at once, so the mouse x must be above -0.4 and
the mouse x must be below 0.4 and the mouse y… etc… The net result of all our
ands is that our variable ‘inbox’ will be 1 if all the conditions are true and 0
if any of the conditions are false. Now we can use this one variable to produce
an effect already, add the following after the above code:
reg01=inbox;
If you wish you can look in the debug window and see it change
from 0 to 1 when your mouse is in the area, but for a much easier indication
simply change the button fill scope code to the following:
Point
point=bnot(point);
x=if(point,-0.4,0.4);
y=0;
red=0.3+reg01*0.2;
blue=0.1;
green=0.1;
Here we are (indirectly) adding the value of our ‘inbox’
variable to the colour to get a pleasant ‘mouse over’ effect. The only thing
left to add now is the actual pressing of the button. For this we will make it
so that the button changes colour when pressed, so modify the control scope to
be:
Init
Bstate=0;
Frame
inbox=
band(
band(above(getkbmouse(1),-0.4),below(getkbmouse(1),0.4)),
band(above(getkbmouse(2),-0.2),below(getkbmouse(2),0.2))
);
reg01=inbox;
Pressing=band(getkbmouse(3),inbox);
bstate=if(pressing,bnot(bstate),bstate);
reg02=bstate;
We have two new variables now, ‘pressing’ is a variable that
will be set to 1 when the user is pressing the button (that is, when the left
mouse button is down and the mouse is in the button area). ‘Bstate’ is the state
of the button, either 1 or 0, we are saying if the button is being pressed,
change the state to not the current state, so if it is 0 it changes to 1 and
vice versa. We are saving the button state in a register because we are going to
use it in the button fill superscope, change the button fill superscope to
become:
Point
point=bnot(point);
x=if(point,-0.4,0.4);
y=0;
red=0.3+reg01*0.2;
blue=0.1+reg02;
green=0.1;
Now our button will change colour when it is pressed. Have you
noticed a problem though? Our button flickers when we press it and seems to
change to a random state, can you tell why this is? It is because of the
following line in our control scope:
bstate=if(pressing,bnot(bstate),bstate);
Because the code is executed every frame, it is switching bstate
to bnot(bstate) every frame we hold down the left mouse button in the area. We
need some way of making the state only change once per press of the mouse key,
there are various ways of achieving this, change the control scope code to the
following:
Frame
inbox=
band(
band(above(getkbmouse(1),-0.4),below(getkbmouse(1),0.4)),
band(above(getkbmouse(2),-0.2),below(getkbmouse(2),0.2))
);
reg01=inbox;
Pressing=band(getkbmouse(3),inbox);
bstate=if(band(pressing,unlocked),bnot(bstate),bstate);
reg02=bstate;
unlocked=bnot(getkbmouse(3));
The new lines are highlighted in red, what we have done is
introduced a variable which is set at the end of the frame, and equals 1 if the
mouse is up, 0 if the mouse is down. Now we only change our state if the
variable ‘pressed’ is 1 and our ‘unlocked’ variable is 1. If you are having
trouble seeing how the ‘unlocked’ variable works, follow the code through from
the user not pressing the button, to the user pressing it for a few frames, then
not pressing it. Remember, the fact that it is at the end of the code is very
important.
Well that is our button completed, just to show a potential use,
let’s add an effect list which is enabled and disabled according to the button
state. Add an effect list, uncheck enabled, check clear every frame, check use
evaluation override, set the input and output blending to maximum and add the
following code:
Frame
Enabled=reg02;
Now add a text object inside the effect list with the text
‘enabled’ and try out your button. Simple or what? Because we used a control
scope and registers, we simply have to reference a register to get the button
state. Effect lists and mouse input are a very powerful combination, experiment
with them to see what you can produce.
Part 9: The Triangle APE
The triangle APE was created by TomyLobo as a way to
render triangles in AVS, given our previous discussion on solid superscopes the
usefulness of such a feature should be obvious. The way in which we render
triangles to the screen is slightly different to a superscope or Texer2 because
we have three coordinates to work with for each triangle; so we reference
x1,y1,x2,y2 and x3,y3 to define the corners of our triangle. For example, start
a new preset, clear every frame and add a Render – Triangle with the following
code:
Init
n=1;
Triangle
x1=-0.5;
y1=-0.5;
x2=0.5;
y2=-0.5;
x3=0;
y3=0.5;
Note that in the triangle ape ‘n’ represents the number of
triangles we want to render, not the number of points. Here we have described a
simple triangle, however explicitly writing the coordinates of a triangle has
limited uses, as we probably want to render a lot of them. Because of the nature
of triangles and defining three coordinates the code tends to be more
complicated than superscopes or Texer2’s and frequently requires loops. Don’t be
frightened though, we have already covered all of the concepts and syntax
involved, we simply have to apply it to a more sophisticated problem.
Let us try to solve a simple problem: rendering a line of
triangles. For most presets involving triangles you would be wise to sit down
with a pen and paper and roughly calculate where you want triangles to be, but
this is a simple problem so we will just jump right in…
We need a row of triangles, so the logical approach would be to
define a single triangle, shift it, render it, shift it, render it etc… So let’s
define our single triangle, something small and offset to start our row, modify
your preset to:
Triangle
x1=-0.9;
y1=0.1;
x2=-0.8;
y2=0.1;
x3=-0.85;
y3=-0.1;
There we go, a little triangle, now if we add a variable that
increases each pass through the triangle code, we can add that to the ‘x’ values
to perform our ‘shift’…
Frame
shift=0;
Triangle
x1=-0.9+shift;
y1=0.1;
x2=-0.8+shift;
y2=0.1;
x3=-0.85+shift;
y3=-0.1;
shift=shift+0.1;
Here we are resetting ‘shift’ to zero each frame, then for each
time the ‘triangle’ code is executed (once per triangle), the value of ‘shift’
is increased by 0.1. Now change the value of ‘n’ in the init box to 18 (18
triangles) and there we have our row! And for a bit of fun let’s make it respond
to the music by changing the value of ‘y3’…
Triangle
x1=-0.9+shift;
y1=0.1;
x2=-0.8+shift;
y2=0.1;
x3=-0.85+shift;
y3=getosc(x1*0.5+0.5,0.1,0);
shift=shift+0.1;
We can also add colour coding to the triangles, however we use
‘red1’, ‘blue1’ and ‘green1’ (bonus points if you can work out why):
Triangle
x1=-0.9+shift;
y1=0.1;
x2=-0.8+shift;
y2=0.1;
x3=-0.85+shift;
y3=getosc(x1*0.5+0.5,0.1,0);
shift=shift+0.1;
red1=abs(x1);
blue1=1-x1;
green1=0.6;
Part 10: Advanced 3D
Well back in Part 4 we covered 3D, but that was only the basics,
now we are going to look at some of the more sophisticated elements of working
in the third dimension. This is the most advanced section of the programming
guide, if you do not feel comfortable tackling the topics here it is best to
spend more time familiarizing yourself with the basics; there’s no point
struggling through and not learning anything. A lot of this part is theory,
which you may find a little disappointing; however the main aim here is to give
you the tools to solve programming tasks that you are likely to encounter.
When working in 3D it is crucial that you fully understand the
environment you are creating, we have already covered rotation matrices and
projections, so you should be at ease with the idea of your three axes x, y and
z defining your world. One of the most useful mathematical tools at our disposal
is vectors, you may or may not be aware of what a vector is, essentially it is a
way to describe a position and a direction. But in our graphics programming we
tend to be less interested in the position, so we usually just use vectors to
describe a direction, for example:
MyVector =
There are lots of notations for vectors, here I am saying the x
component of my vector is 0, the y component is 0 and the z component is 1 (this
is a mathematical notation not AVS code). If you imagine a 3D space, the vector
‘MyVector’ is describing a direction of positive z, i.e. pointing down the z
axis.
MyVector =
Now MyVector is describing a direction 45 degrees between the y
and z axes.
Hopefully this concept is fairly straightforward; now, one thing
we might want to do with a vector is calculate its length. We do this using
Pythagoras theorem which you may be familiar with; I don’t want to go into much
detail about it, rather I will just state the equation:
“Aaah! The
equations have started!” I hear you cry. Don’t worry its very easy; it simply
says ‘the length of a vector equals the square root of the sum of the squares of
the components.’ In AVS this would be:
lengthv=sqrt(sqr(vx)+sqr(vy)+sqr(vz));
Where your vector is defined by vx, vy, vz.
Now when we do our calculations with vectors it is rather nice
to have them normalized, what does that mean? It means that the x, y and z add
up to 1 (a vector of length 1 is known as a ‘unit vector’), the way we achieve
this is by dividing each component by the length of the vector. So in AVS code
that would just be:
vx=vx/lengthv;
vy=vy/lengthv;
vz=vz/lengthv;
Right, now we know a little about vectors we can start looking
at some uses for them; so let’s move onto normals. What is a normal? A normal is
related to a face, if you have a face, the normal of that face is a vector which
defines the direction it is facing. For example, hold out your hand with your
palm flat, if you imagine your palm is a face (such as a triangle or square) the
normal to your palm is a vector that describes the direction away from your
hand.
Normals are very useful for a variety of reasons; imagine, for
example you want to light a cube, if you only know the location of all the faces
on the cube you don’t really have enough data to produce any decent lighting. If
you know the direction of each face, you can tell whether or not each side is
facing the light or not.
Let’s make a preset that renders a solid 3D square and shows its
normal as a line, start a new preset, and add a superscope with the following
code:
Init
Frame
rotx=rotx+0.01;
roty=roty+0.02;
rotz=rotz+0.03;
reg00=rotx;
reg01=roty;
reg02=rotz;
Beat
Point
This is a very simple ‘control scope’ that we are using to store
our rotation values, we need this because the values will be used in more than
one superscope (go back to part 5 if you are unfamiliar with registers). Now add
another superscope to render a square:
Init
n=1000;
Frame
rotx=reg00;
roty=reg01;
rotz=reg02;
linesize=2;
drawmode=2;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
switch=0.5;
Beat
Point
switch=-switch;
y1=i-0.5;
x1=switch;
z1=0;
x2=x1*crotz-y1*srotz;y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;z1=y2*srotx+z3*crotx;
z2=1/(z1+2);
x=x3*z2;
y=y1*z2;
You should have no problem understanding this code, if you do,
go back to part 4 and refresh your memory on the basics of 3D (this is actually
the solution to exercise 4C and 5A). You may have noticed that my rotation
matrices are a little crushed together, this is just to save space and make the
code a bit smaller.
Okay so we have a rotating square, now we need to define the
normal to that face. Because our face is flat to the z axis (z1=0) we actually
already know our normal, it is z=1 or z=-1. Why can it be two values? Because it
is just a square on its own it can be ‘facing’ either up or down it doesn’t
really matter, we just have to make a decision and stick to it. If the square
was part of an object such as a cube we would want it to face away from the
centre of the cube, and we would assign its normal based on that. So let us say
arbitrarily that the normal to this face is z=1, in order to display that as a
line we need to put the normal through rotation matrices with the same rotation
as the face. This is because as the face rotates the normal is also rotating, so
we need to recalculate it.
Add a new superscope with the following code:
Init
n=2;
Frame
linesize=2;
drawmode=2;
rotx=reg00;
roty=reg01;
rotz=reg02;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
Beat
Point
y1=0;
x1=0;
z1=0;
x2=x1*crotz-y1*srotz;y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;z1=y2*srotx+z3*crotx;
z2=1/(z1+2);
x=x3*z2;
y=y1*z2;
This second superscope does not do anything yet, but 90% of our
work is done, because all we have to do is add the little bit of code that draws
a line from the origin to the point defined by the normal, i.e. from 0,0,0 to
0,0,1, replace the code with:
Init
n=2;
Frame
linesize=2;
drawmode=2;
rotx=reg00;
roty=reg01;
rotz=reg02;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
point=0;
Beat
Point
y1=0;
x1=0;
z1=point;
point=bnot(point);
x2=x1*crotz-y1*srotz;y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;z1=y2*srotx+z3*crotx;
z2=1/(z1+2);
x=x3*z2;
y=y1*z2;
red=1;
blue=0;
green=0;
There we have it, our face and normal. This was a rather simple
case because we know our normal, plus it was centred at the origin, however I
hope you now understand the concept a little better.
In the example we just tried, we already knew the normal of our
face, it was obvious, this is often the case if you are making simple geometric
shapes; for example a cube will have faces where the normals are x=1, x=-1, y=1
etc… unless you have defined it without the faces aligned to the axis. However
what can you do if can’t just guess the normal of a face? The answer is a rather
nifty formula/equation known as the cross product. If you have a face, you can
define two vectors which lie on that face and calculate the cross product to
give you the normal. The cross product is defined as:
Cx = v1(y) * v2(z) - v1(z) * v2(y)
Cy = v1(z) * v2(x) -
v1(x) * v2(z)
Cz = v1(x) * v2(y) - v1(y) * v2(x)
Where you provide the vectors v1 and v2, and the result is the
vector Cx,Cy,Cz (a vector which is perpendicular to v1 and v2). So how do you
define two vectors that lie on the face? Well it’s really quite easy, you simply
take two points which are on the face (you might as well use vertices since you
already have them) and subtract them to get a vector which lies on the
face.
For example, we have a triangle which is the face whose normal
we wish to calculate with vertices:
x1 y1 z1 (Vertex
1)
x2 y2 z2 (Vertex 2)
x3 y3 z3 (Vertex
3)
(A vertex simply means a corner just in case you didn’t know)
There is our triangle in 3D, to calculate the normal we make 2 vectors from
vertex 1 to 2 and vertex 1 to 3:
v1x=x2-x1;
v1y=y2-y1;
v1z=z2-z1;
v2x=x3-x1;
v2y=y3-y1;
v2z=z3-z1;
So we have our two vectors, which we found by subtracting some
of the vertices from each other, we can chose a different combination of
vertices as long as we end up defining two vectors that lie on the face. Now we
plug those vectors into the cross product formula to find the normal:
nx = v1y*v2z - v1z*v2y;
ny = v1z*v2x - v1x*v2z;
nz =
v1x*v2y - v1y*v2x;
There is our normal, defined by nx,ny,nz; although at this point
it would be a good idea to normalize it because it may be very large or small
depending on the size of the triangle; we already covered that though so I won’t
repeat it here. If you want to see this in action, you can load the preset
‘Demonstration – Arbitrary Normal’ in your AVS Programming Guide directory. The
preset creates a random triangle and shows its normal, you will notice that the
line for the normal is based at the origin not on the triangle; this is because,
if you remember, we are interested in using the normal to define a direction, we
are not interested in the location. You should be able to read through the code
and follow the steps we have just been through.
The final topic we are going to explore is the dot product, a
formula that will tell us the angle between two vectors. We already know the
direction of a face, but currently we only have it expressed as a vector; in a
realistic programming context this isn’t very useful because we almost always
want to know the angle between the normal of that face and something else. For
example, if we want to know if a face is pointing away from the camera (so that
we can avoid rendering it for speed) want we need is the angle between the face
normal and a vector from the camera to the face.
The formula for the dot product of two vectors is:
Dot=(v1x*v2x+v1y*v2y+v1z*v1z)
In other words multiply each vector component together and sum
the result, the value that you get as a result is the cosine of the angle
between the vectors (multiplied by their lengths but that isn’t important for
our needs). So in laymen’s terms the result is a value between -1 and 1 where 1
means they are facing each other, -1 means they are facing the same direction
and 0 means they are at 90 degrees to each other (at least that’s all we really
care about). This is another situation where normalizing the vectors is
important, otherwise the result you get will not fall between -1 and 1.
Have you ever heard of Lambert's cosine law? Probably not, but
it states that the apparent brightness of a face is proportional to the cosine
of the angle between the normal of the face and the direction of the light.
Gosh, that’s convenient isn’t it, so if we define a light as a vector (the way
the light is pointing) and set the colour of our face to the dot product of the
face normal and light vector, the face will be lit pretty accurately without any
extra work on our part.
Let’s make a rotating square with some lighting to demonstrate
this idea, make a new preset and add a superscope with the following code:
Init
Frame
n=1000;
linesize=2;
drawmode=2;
rotx=rotx+0.01;
roty=roty+0.02;
rotz=rotz+0.03;
crotx=cos(rotx);srotx=sin(rotx);croty=cos(roty);
sroty=sin(roty);crotz=cos(rotz);srotz=sin(rotz);
switch=0.5;
Beat
Point
switch=-switch;
y1=i-0.5;
x1=switch;
z1=0;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;
z1=y2*srotx+z3*crotx;
z2=1/(z1+2);
x=x3*z2;
y=y1*z2;
There is a simple rotating square, now the first thing we need
to add to do some lighting is calculate the normal, we already know how to do
this, so add the following code to the end of the frame box:
//define the normal
nx=0;ny=0;nz=1;
//rotate the
normal
x1=nx;y1=ny;z1=nz;
x2=x1*crotz-y1*srotz;
y2=x1*srotz+y1*crotz;
x3=x2*croty+z1*sroty;
z3=-x2*sroty+z1*croty;
y1=y2*crotx-z3*srotx;
z1=y2*srotx+z3*crotx;
So now we can access the rotated normal as a vector (x3,y1,z1),
again we specified the normal to be z=1 however z=-1 would have been fine too.
Next we need to define our light as a vector, we will define it and then
normalize it; add the following code to the frame box:
//define the light
lx=1;
ly=0;
lz=0;
//normalise
it
length=sqrt(sqr(lx)+sqr(ly)+sqr(lz));
lx=lx/length;
ly=ly/length;
lz=lz/length;
So this is our light, a vector that points along the positive x
axis. Now you can hopefully see that we don’t really need to normalize this
vector because the sum of its components is already 1. The reason we are putting
the code in is so that you can fiddle with the values of the light vector, for
example we could make the light point at a 45 degree angle between the x and y
axes by setting lx and ly to 1, the normalization code would turn it into a unit
vector. Notice that we did not rotate the light vector, this is because we want
the light to be stationary and shine on the rotating face, if we rotated the
light vector with the same rotation as the face (like we did for the normal) it
would be as if the light was attached to the face, so the lighting would not
change.
Now the final stage is to dot product the light vector with the
normal of the face to find out the angle between them, so add the following code
to the end of the frame box:
//dot product the light and the
normal
dot=x3*lx+y1*ly+z1*lz;
This should be nothing too surprising, we are simply plugging
the face normal and light vectors into the dot product formula presented
earlier. So how do we use this value? Well we can simply assign the colour
values of the superscope directly to that variable, since ‘dot’ will by 1 when
facing the light and <0 when facing away from it. Add the following code to
the end of the point box:
red=dot;
blue=dot;
green=dot;
All done! Notice how the face appears to be lit as it faces our
virtual light, you can fiddle with the values to see the effect they have,
setting the light to 0,0,1 (a vector pointing down the positive z axis) will
have the effect of lighting in the direction of the camera which may help you to
see the effect.
Well there is your introduction to lighting; here we have seen
one type of simple lighting, the best analogy for the effect we have created is
a ‘sun’, because our faces are lit uniformly depending on rotation. In other
words the location of our face isn’t important, as if the light were very far
away, but very bright. A more realistic lighting system would account for the
distance between the light and the face, something you already have the ability
to do because of what we have covered regarding vectors. One method for finding
the distance between the light and the face would be:
* Define the light location (a)
* Define the face location
(b)
* Subtract (a) from (b) to create a vector (c)
* Find the length of
(c) using Pythagoras
That length value is the distance between the light and the
face, you would use this value to modify the colour of your face. We won’t go
into any more detail about this though, since it is up to you to
experiment.
Part 11: Tips & Tricks
This section contains some useful coding tips and tricks that do
not apply to any specific object in AVS, but are handy for polishing your
presets and solving certain problems.
Sine Waves
Sinusoidal effects are very useful in AVS; sinusoidal motion is
very aesthetically pleasing to the human eye and occurs regularly in nature. It
is important to understand the basics of sin and cos to use them effectively in
presets, the (basic) equation of a sine wave
is:
y=?sin(2?fx)
That is,
amplitude times sin of 2 times pi times the frequency times x. So to produce a
sine wave of amplitude 0.5 and frequency 2 we can make a superscope
containing:
Point:
x=2*i-1;
y=0.5*sin(2*$pi*2*x);
Remember that the x axis in AVS spans from -1 to 1 so you will
see 4 periods rather than 2 as you might expect. Experiment a little with sine
waves and sinusoidal motion so that you are able to plan and implement the
effect you want, rather than throwing sin’s and cos’s into code randomly.
Aspect ratio correction
To aspect ratio correct a preset, for example a DM or
superscope, the simplest method is:
Frame:
asp=h/w;
Point/Pixel:
x=x*asp;
Where x is your final x after translation, rotation etc… However
this relies on the AVS window being wider than it is tall, otherwise the data
will probably spill off the edge of the screen. You can apply more complex
functions to create an aspect ratio correction for any size window:
Point/Pixel:
x=x*if(above(w,h),h/w,1);
y=y*if(above(h,w),w/h,1);
Frame rate independent movement
To avoid your preset moving too fast on fast computers you can
limit the movement of objects speed by basing it on time rather than the frame
rate.
Frame:
passed=gettime(0)-last;
MyVar=MyVar+passed;
last=gettime(0);
Where your movement is based on MyVar. To have different
elements moving at different speeds you can scale this value ‘passed’, for
example multiplying it by a random scale changed on beat.
Inactive effect lists
Effect lists that fade in and out or have changing alphaout of
any kind should be disabled once the alphaout becomes unnoticeably small, thus
improving the frame rate:
Frame:
enabled=above(alpha,0.01);
alphaout=alpha;
Caching static data
Static data, for example a gradient background to a preset can
be ‘cached’ into a buffer at the start of a preset to improve the frame rate.
Simply put the effects to be cached into an effect list with a buffer save at
the end, then use the following code in the effect list:
Init
hi=0;
wi=0;
Frame:
enabled=bor(1-equal(hi,h),1-equal(wi,w));
hi=h;
wi=w;
This will enable the preset when the preset starts and (more
importantly) when the AVS window is resized (so that the data can be
recalculated which is probably necessary). You can use a buffer save with
‘restore’ to get your saved data at any point.
Part 12: Advanced Exercises
It is expected that by this chapter you have gained a fairly
solid understanding of AVS programming and are able to translate most of your
ideas into code. After all, the reason for learning the AVS language is to allow
you to better implement your ideas and reproduce what you envisage in your mind.
One of the greatest skills any programmer needs is problem solving, given a
problem you need to be able to come up with a creative and graceful solution,
that is the real joy of programming.
This final chapter contains some exercises that you should find
challenging, read the tips if you struggle, but try to work it out on your own
first. Also there is usually more than one solution to a problem, so do not
think your solution must match mine, provided it works. Hopefully this will
encourage you to think creatively and develop your own approach to solving AVS
problems. And when in doubt… use the expression help!
Exercises:
Create a dot superscope that produces a solid 10 pixel by 10
pixel square in the centre of the AVS window. The square should remain 10x10
regardless of the AVS window size.
(Exercise 12A)
Create a 10,000 point superscope that displays the oscilloscope
data, but only updates once a
second. (Exercise 12B)
Tips for Exercise 12A:
* First decide how to define 1 pixel, it will be related to the
height and width of the AVS window.
* Try to produce something of a fixed
width before you tackle 2 dimensions.
* Use the solid superscope base from
chapter 2, but rearrange it to your needs
* An ideal solution will have
n=100, does yours?
Tips for Exercise 12B:
* Break the problem into smaller problems… can you make a
variable equal 1 when a second has elapsed?
* You need to use the megabuf to
store the oscilloscope values
* What is the maximum limit for a loop (I’ll
give you a clue, it’s below 10,000!)? If you can’t use one loop, maybe you can
use two.
* Sample the oscilloscope data using getosc(), divide your loop
counter by 10,000 for the band.
52