WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 2 | 3 || 5 | 6 |

«Том Миллер Managed DirectX*9 Программирование графики и игр **омпэ*> Предисловие Боба Гейнса Менеджера проекта DirectX SDK корпорации Microsoft SAMS [Pi] KICK START Managed DirectX 9 ...»

-- [ Страница 4 ] --

Запись xg недействительна, поскольку смешиваются компоненты разных типов. Переменные в языке шейдеров могут иметь модификаторы похо­ жие на модификаторы языка С или С#. Вы можете объявлять кон­ станты тем же самым образом, что и ранее: const float someConstant = 3. Of;

Вы можете совместно использовать переменные в различных про­ граммах: shared float someVariable = l.Of;

При необходимости более подробую информацию относительно HLSL можно найти в документации DirectX SDK.

Использование шейдеров для рендеринга, использование техник «TECHNIQUE» Теперь, когда мы написали программу вершинного шейдера, необхо­ димо определить «точки входа» в данную программу и написать соот­ ветствующий алгоритм. Для этого мы можем использовать процедуру, называемую «техника» («technique»). Под техникой понимается способ (или последовательность кода), реализующий ту или иную функцию шейдера. Такой прием позволяет осуществить один или несколько про­ ходов, каждый раз с помощью кода HLSL определяя состояние устрой­ ства, а также вершинные и пиксельные шейдеры. Рассмотрим простой алгоритм, который применим к нашему приложению. Добавьте следую­ щий код к вашему файлу simple.fx: technique TransformDiffuse { pass PO { S Зак. Часть III. Более совершенные методы построения графики CullMode = None;

// shaders VertexShader = compile vs_l_l Transform();

PixelShader = NULL;

Здесь мы объявляем технику TransformDifruse, позволяющую преоб­ разовать вершину и добавить диффузный цвет. В данной технике опре­ делен только один цикл. Режим отбрасывания невидимых сторон Cull не включен, так что мы можем видеть обе стороны нашего единственного треугольника. Если вспомнить, мы уже писали аналогичный код на язы­ ке С#. Теперь мы попробуем «запрограммировать» обработку вершин, ис­ пользуя вершинный шейдер версии 1.1 (в программе vs_l_l). При ис­ пользовании версии 2.0 в программе нужно указать vs_2_0. Пиксельный шейдер определяется как ps_2_0. Так как мы не используем здесь обработку пиксела, мы определяем: PixelShader = NULL. Далее мы должны переписать С# код, чтобы ис­ пользовать эту технику и соответствующий цикл. Для начала используем объект Effect HLSL-кода, объявленный ранее как основной. Добавьте следующие строки к методу InitializeGraphics (после создания устройства):

// Create our effect effect = Effect.FromFile(device, @"..\..\simple.fx", null, ShaderFlags.None, null);

effect.Technique = "TransformDiffuse";

Объект Effect создается из файла simple.fx. Затем мы объявляем тех­ нику Effect для ее использования в нашей HLSL-программе. Если вы по­ мните, у нас имелись две переменные, которые необходимо было переза­ писывать каждый раз при смене кадра. Поэтому добавим следующий код в конце метода UpdateWorld:

Matrix worldViewProj = worldMatrix * viewMatrix * projMatrix;

effect.SetValue("Time", (float)Math.Sin(angle / 5.Of));

effect.SetValue("WorldViewProj", worldViewProj);

Здесь мы объединяем наши сохраненные матрицы и изменяем пере­ менные в коде HLSL. Этот метод вызывается каждый раз при смене кадра, поэтому переменные будут обновляться автоматически. Теперь осталось переписать процедуру рендеринга, используя программу вершинного шейдера. Перепишите ваш метод DrawPrimitives следующим образом:

Глава 11. Введение в программируемый конвейер, язык шейдеров int numPasses = effect.Begin(O);

for (int i = 0;

i < numPasses;

i++) { effect.Pass(i);

device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);

} effect.End();

( Вначале мы вызываем метод effectBegin для нашей техники Effect. Используя флажки для этого метода, мы могли бы выбрать режим, позво­ ляющий не сохранять отдельные состояния, но для этого примера это не так важно. Метод возвращает число циклов в используемой технике и создает соответствующий цикл рендеринга (в нашем случае один). Перед тем как создать рисунок, необходимо вызвать метод прохода или цикла Pass для нашего объекта. Единственный принимаемый в этом методе пара­ метр — индекс i. Данный метод подготавливает устройство к процедуре рендеринга для обозначенного цикла, обновляет состояние устройства и устанавливает методы для вершинных и пиксельных шейдеров. И, нако­ нец, знакомый нам метод DrawPrimitives рисует примитивы (в нашем случае треугольник). В конце метода необходимо добавить команду завершения efFectEnd. При запуске приложения на экране отобразится вращающийся треуголь­ ник с последовательно изменяющимися цветами, см. рис. 11.1.

Рис. 1 1. 1. Красочный вращающийся треугольник Часть III. Более совершенные методы построения графики Использование программируемого конвейера для рендеринга mesh-объектов Пример с отображением треугольника является чересчур простым. Попробуем отобразить объект mesh, используя программируемый кон­ вейер. Для этого заменим переменные в разделе объявления вершин и вер­ шинном буфере на следующее:

private Mesh mesh = null;

Одно из преимуществ mesh-объектов состоит в том, что при ренде­ ринге они автоматически задают параметр объявления вершин vertex declaration. Поэтому в нашем случае необходимо убрать в используемом коде все ссылки на вершинные буферы или параметры объявления вер­ шин. Необходимо также изменить метод инициализации для создания meshобъекта, для этого после создания устройства и объекта добавьте следу­ ющий код:

// Create our cylinder mesh = Mesh.Cylinder(device, 1.5f, 1.5f, 1.5f, 36, 36);

И в завершении необходимо изменить метод рисования объекта. За­ мените вызов метода DrawPrimitives на DrawSubset:

mesh.DrawSubset(0);

Все достаточно просто. Теперь при запуске приложения на экране ото­ бразится плоский цилиндр с теми же цветами, которые были использо­ ваны при рендеринге треугольника. На самом деле, цилиндр не совсем напоминает цилиндр, поскольку он освещен светом со всех сторон и не кажется объемным. Раньше мы просто добавляли направленное освеще­ ние, и все выглядело достаточно реалистично. Но при этом мы использо­ вали непрограммируемый конвейер, включающий в себя установку ос­ вещения. В нашем случае мы используем программируемый конвейер, поэтому необходимо изменить код HLSL, чтобы отобразить единственный источ­ ник направленного белого света. Для начала нам понадобится переменная, описывающая направление света. Добавьте ее к вашему коду HLSL:

Глава 11. Введение в программируемый конвейер, язык шейдеров // The direction of the light in world space float3 LightDir = (O.Of, O.Of, -l.Of);

Теперь мы можем заменить существующий метод на следующий:

// Transform our coordinates into world space VSJIUTPUT Transform) float4 Pos : POSITION, float3 Normal : NORMAL ) { // Declare our return variable VSJXJTPUT Out = (VSJXJTPUT) 0;

// Transform the normal into the same coord system float4 transformedNormal = mul(Normal, WorldViewProj);

// Set our color Out.diff.rgba = l.Of;

Out.diff *= dot(transformedNormal, LightDir);

// Transform our position Out.pos = mul(Pos, WorldViewProj);

// Return return Out;

} Обратите внимание, что в метод включен новый параметр, а именно данные о нормали («normal»). Необходимо убедиться, что каждая вер­ шина, которую мы прогоняем в процессе обработки, имеет данные о нор­ мали, иначе это вызовет ошибку. Цилиндр, созданный классом mesh, уже содержит эту информацию, так что все в порядке. Далее необходимо преобразовать координаты нормали в ту же самую систему координат, в которой будет описываться вершина, поскольку мы не сможем рассчитать освещение, имея различные системы координат для наших данных. В литературе по DirectX SDK можно найти следующую информацию, касательно освещения: направленный свет рассчитывается с помощью скалярного произведения нормали к вершине на вектор направления ос­ вещения, умножая это на цвет индикатора. Вы можете установить значение «l.Of» для компонента цвета (r,g,b,a) и произвести это вычисление, чтобы получить конечный цвет цилиндра. Теперь при запуске приложения на экране отобразится вращающийся белый цилиндр. Цилиндр выглядит более реалистично чем раньше, см. рис.11.2.

Часть III. Более совершенные методы построения графики Рис. 11.2. Вращающийся цилиндр, выполненный на языке шейдеров Изменяя цвет освещения можно получать различные цветовые эффекты.

Использолвание языка HLSL для создания пиксельного шейдера Вершинные шейдеры — это только часть того, что можно реализо­ вать, используя программируемый конвейер. Было бы весьма заманчиво рассмотреть цвета каждого пиксела. Для этого мы возьмем пример MeshFile, рассмотренный нами в главе 5 (Рендеринг mesh-объектов) и изменим его с помощью программируемого конвейера. В примере из гл. 5 помимо обработки вершин мы использовали обработку данных тек­ стур, поэтому попытка применить пиксельный шейдер может оказаться весьма наглядной. После загрузки данного проекта нам нужно будет внести несколько изменений в исходник программы, чтобы можно было использовать про­ граммируемый конвейер. Вначале объявляем переменные для объекта Effect и матриц преобра­ зования:

private Effect // Matrices private Matrix private Matrix private Matrix effect = null;

worldMatrix;

viewMatrix;

projMatrix;

Глава 11. Введение в программируемый конвейер, язык шейдеров Необходимо также переписать метод инициализации эмулируемого устройства в случае, если графическая карта не поддерживает шейдеры. Метод инициализации приведен в листинге 11.2.

Листинг 11.2. Инициализация графики, проверка поддерживаемых режимов (Fallback). public bool InitializeGraphics () ( // Set our presentation parameters PresentParameters presentParams = new PresentParameters();

presentParams.Windowed = true;

presentParams.SwapEffeet = SwapEffect.Discard;

presentParams.AutoDepthStencilFormat = DepthFormat.D16;

presentParams.EnableAutoDepthStencil = true;

bool canDoShaders = true;

// Does a hardware device support shaders? Caps hardware = Manager.GetDeviceCaps(3, DeviceType.Hardware);

if ((hardware.VertexShaderVersion >= new VersionU, 1)) && (hardware.PixelShaderVersion >= new Version(1, 1))) { // Default to software processing CreateFlags flags = CreateFlags.SoftwareVertexProcessing;

// Use hardware if it's available if (hardware.DeviceCaps.SupportsHardwareTransformAndLight) flags = CreateFlags.HardwareVertexProcessing;

// Use pure if it's available if (hardware.DeviceCaps.SupportsPureDevice) flags |= CreateFlags.PureDevice;

// Yes, Create our device device = new Device(0, DeviceType.Hardware, this, flags, presentParams) } else ( // No shader support canDoShaders = false;

// Create a reference device device = new Device(0, DeviceType.Reference, this, CreateFlags.SoftwareVertexProcessing, presentParams);

I // Create our effect effect = Effect.FromFile(device, @"..\..\simple.fx", null, ShaderFlags.None, null);

effect.Technique = "TransformTexture";

// Store our project and view matrices projMatrix = Matrix.PerspectiveFovLH((float)Math.PI / 4, Часть III. Более совершенные методы построения графики this.Width / this.Height, Of, 10000.Of);

viewMatrix = Matrix.LookAtLH(new Vector3(0,0, 580.Of), n w Vector3(), e n w Vector3(0,1,0));

e // Load our mesh LoadMesh(@"..\..\tiny.x");

return canDoShaders;

} Данный метод аналогичен методу инициализации, который мы уже использовали в этой главе, только помимо вершинных шейдеров здесь проверяется возможность поддержки и пиксельных шейдеров (как и рань­ ше, возвращается булева переменная). Для изменения основного метода Main мы можем обратиться к тому же алгоритму, который использовали в этой главе при отображении треугольника. Необходимо отметить, что, поскольку метод SetupCamera использует­ ся только в непрограммируемом конвейере, мы можем исключить его из программы при преобразовании координат или установке освещения. Естественно при этом необходимо также удалить соответствующий вы­ зов в методе OnPaint. И последнее, мы должны заменить вызов рисунка DrawMesh соответ­ ствующим кодом рендеринга, написанном на языке HLSL и приведен­ ном в листинге 11.3.

Листинг 11.3. Код вызова рисунка mesh-объекта, написанный на языке HLSL. private void DrawMesh(float yaw, float pitch, float roll, float x, float y. float z) { angle += O.Olf;

worldMatrix = Matrix.RotationYawPitchRoll(yaw, pitch, roll) * Matrix.Translation(x, y, z);

Matrix worldViewProj = worldMatrix * viewMatrix * projMatrix;

effect.SetValue("WorldViewProj", worldViewProj);

int numPasses = effect.Begin(O);

for (int iPass = 0;

iPass < numPasses;

iPass++) { effect.Pass(iPass);

for (int i = 0;

i < meshMaterials.Length;

i++) { device.SetTexturefO, meshTextures[i]);

mesh.DrawSubset (i);

} } effect.End();

} Глава 11. Введение в программируемый конвейер, язык шейдеров Данный метод вызывается при каждой смене кадра. Как видно из ме­ тода, мы объединяем матрицы преобразования и изменяем в соответствии с этим код HLSL. Затем мы отображаем каждое подмножество объекта mesh для каждо­ го цикла техники. Однако, мы до сих пор не объявили и не создали для этого приложения исходник на языке HLSL, так что необходимо доба­ вить новый пустой файл «simple.fx» и добавить код, приведенный в лис­ тинге 11.4.

Листинг 11.4. Код HLSL для рендеринга текстурных объектов. // The world view and projection matrices float4x4 WorldViewProj : WORLDVIEWPROJECTION;

sampler TextureSampler;

// Transform our coordinates into world space void Transform( in float4 inputPosition : POSITION, in float2 inputlexCoord : IEXCOORD0, out float4 outputPosition : POSITION, out float2 outputTexCoord : TEXCOORDO ) { // Transform our position outputPosition = mul(inputPosition, WorldViewProj);

// Set our texture coordinates outputTexCoord = inputlexCoord;

} void TextureColor( in float2 textureCoords : TEXCOORDO, out float4 diffuseColor : COLORO) { // Get the texture color diffuseColor = tex2D(TextureSampler, textureCoords);

};

technique TransformTexture { pass PO { // shaders VertexShader = compile vs_1_1 Transform();

PixelShader = compile ps_1_1 TextureColor();

} } Часть III. Более совершенные методы построения графики Легко заметить, чем отличаются программы шейдеров. Вместо сохра­ нения возвращаемых значений в структуре, мы просто добавили выход­ ные параметры в раздел объявления метода. Для программирования вершины нас интересуют только данные ее положения и координаты текстуры. Используемый нами пиксельный шейдер принимает координаты тек­ стуры для соответствующего пиксела и возвращает цвет данного отобра­ жаемого пиксела. Для нашего случая мы используем заданный по умолчанию цвет тек­ стуры, и встроенный метод tex2D производит выборку текстуры в дан­ ном наборе координат и возвращает цвет в этой точке. Результат использования пиксельного шейдера для нашего mesh-объек­ та приведен на рис. 11.3.

Рис. 11.3. Результат рендеринга mesh-объекта с использованием пиксельного шейдера На самом деле, глядя на рисунок, мы не видим пока каких-либо суще­ ственных изменений по сравнению с картинками, построенными на базе непрограммируемого конвейера. Попробуем использовать еще одну тех­ нику для HLSL-программы. Добавьте следующий метод к вашему коду HLSL:

void InverseTextureColor( in float2 textureCoords : TEXCOORDO, Глава 11. Введение в п р о г р а м м и р у е м ы й конвейер, язык шейдеров out float4 diffuseColor : COLORO) { // Get the inverse texture color diffuseColor = 1.Of — tex2D(TextureSampler, textureCoords);

};

technique TransformInverseTexture { pass PO { // shaders VertexShader = compile vs_l_l Transform();

PixelShader = compile ps_l_l InverselextureColor();

} } Данный код аналогичен нашему первому пиксельному шейдеру с той лишь разницей, что теперь при определении цвета мы вычитаем выбо­ рочный цвет из значения «1.Of». Поскольку значение «lOf» рассматрива­ ется как «fully on» для цвета, произведя вычитание, мы обращаем цвет пиксела. Помимо этого мы будем использовать еще одну технику TransformlnverseTexture, которая отличается только методом вызываемо­ го шейдера. Теперь нам необходимо переписать наш основной С# код, чтобы иметь возможность переключать эти методы. Для этого необходимо написать обработку нажатия клавиш «1» и «2» на клавиатуре:

protected override void OnKeyPress(KeyPressEventArgs e) { switch (e.KeyChar) { case '1': effect.Technique = "TransformTexture";

break;

case '2': effect.Technique = "TransformlnverseTexture";

break;

} base.OnKeyPress (e);

} Теперь запустите приложение и нажмите клавишу «2». Объект теперь выглядит как негатив, рис.11.4. Пример программы, позволяющей изменять цвета объекта, включен в прилагаемый CD диск.

Часть III. Более совершенные методы построения графики Рис. 11.4. Рендеринга mesh-объекта с обращенными цветами Краткие выводы В этой главе бьши рассмотрены основные особенности языка HLSL. включая. • Использование программируемого конвейера. • Преобразование вершин с помощью шейдеров. • Использование пиксельных шейдеров. В следующей главе мы рассмотрим более совершенные методы языка HLSL.

Глава 12. Использование языка шейдеров HLSL Глава 12. Использование языка шейдеров HLSL В предыдущей главе мы ввели понятие программируемого конвейера и рассмотрели его возможности в сравнении с непрограммируемым кон­ вейером. В этой главе мы рассмотрим особенности использования языка шей­ деров HLSL для создания и отображения на экране более реалистичных картинок, нежели те, которые мы получали ранее, используя непрограм­ мируемый конвейер. Мы коснемся следующих вопросов. • Простая анимация вершины. Простая цветная анимация. • Объединение цвета текстуры с цветами поверхности. • Световые модели с текстурами. • Различие между повершинным per-vertex и попиксельным per-pixel наложением света.

Использование простых формул для моделирования анимации Для примера мы возьмем цилиндр, рассмотренный нами в предыду­ щей главе, затем слегка изменим написанную для него программу, чтобы продемонстрировать возможность простой анимации с помощью програм­ мируемого конвейера. Мы попробуем отобразить качающийся объект. Как только мы загрузили код из предыдущего примера, первое, что нам необходимо сделать, — это выбрать сферу вместо цилиндра. Это вовсе не означает, что мы не можем работать с цилиндром, просто сфера лучше смотрится. Замените код создания цилиндра на следующий:

// Create our sphere mesh = Mesh.Sphere(device, 3.Of, 36, 36);

Поскольку размер нашей сферы больше, чем у вышеупомянутого ци­ линдра, необходимо изменить матрицу вида. Для этого нам необходимо переместить камеру назад, описав эту процедуру следующим образом:

viewMatrix = Matrix.LookAtLH(new Vector3(0,0, 9.Of), new Vector3(), new Vector3(0,1,0));

Теперь, запуская приложение, мы должны увидеть монотонно враща­ ющуюся сферу, освещенную единственным направленным источником Часть III. Более совершенные методы построения графики света. Для создания эффекта анимации попробуем в коде шейдера заме­ нить строку, описывающую преобразование, следующим кодом:

// Store our local position float4 tempPos = Pos;

// Make the sphere 'wobble' some tempPos.y += cos(Pos + (Time * 2.0f));

// Transform our position Out.pos = mulftempPos, WorldViewProj) ;

Обратите внимание, что переменная Time используется уже неоднок­ ратно в наших примерах. Для данного примера необходимо переопреде­ лить эту переменную, добавив ее к коду шейдера: float Time = O.Of;

Кроме того, необходимо перезаписывать эту переменную при каждой смене кадра, поэтому в главном коде, в конце метода UpdateWorld, до­ бавьте следующий код: effect.SetValue("Time", angle);

Теперь опишем наши последние действия. Вначале исходное положе­ ние (входной параметр) вершины сохраняется как переменная. Затем ком­ понента Y этой переменной модулируется путем добавления косинуса от суммы текущего местоположения вершины и значения Time*2.0f. Так как косинус меняется в пределах от -1 до +1, функция будет периодичес­ ки повторятся. Переменная Time влияет на результат анимации в боль­ шей степени, нежели значение расположения Pos. Можно добавить еще одно новшество, например, переменную анима­ ции для цвета. Замените код инициализации Out.diff следующим:

// Set our color Out.diff = sin(Pos + Time);

Очевидно, данный код аналогичен только что описанному методу вер­ шинной анимации. Единственное различие — использование функции синуса, который, судя по качеству отображаемой на экране картинки, предпочтительнее, чем косинус. Использование языка HLSL позволяет быстро применять различные способы отображения картинки без существенных изменений основного кода.

Глава 12. Использование языка шейдеров HLSL Объединение цветов текстуры с цветами поверхности На практике встречается не так много приложений, использующих простые псевдослучайные цвета. Как правило, модели создаются или с помощью сложных текстур, или из нескольких текстур. Допустим, име­ ется сценарий, где необходимо смешать или наложить две или более тек­ стуры. Бесспорно, это можно было бы сделать и на непрограммируемом конвейере. Но допустим, мы используем объект, похожий на «tiny.x», ко­ торый мы уже неоднократно использовали в нашей книге. Данная мо­ дель использует только одну текстуру. Теперь попробуем для этой моде­ ли реализовать две текстуры, объединяя цвет текстуры с цветом поверх­ ности (иногда используется термин интерполяция текстуры). Для этого используем пример текстуры объекта, написанный в пре­ дыдущей главе. Попробуем добавить вторую текстуру, смешать эти тек­ стуры и выдать окончательный цвет. ПРЕДЕЛ ДОСТУПНЫХ КОМАНД ДЛЯ ПИКСЕЛЬНОГО ШЕЙДЕРА Поскольку мы будем манипулировать пиксельным шейдером, не­ обходимо обратить внимание на то, что количество доступных ко­ манд, которые могут быть использованы в шейдерах версии 1.1, чрезвычайно ограничено. Например, для шейдеров версий не стар­ ше 1.4 мы ограничены 12-ю командами в пределах всей програм­ мы. Для версии 1.4 мы имеем уже 28 команд и до 8 констант. Версия шейдера 2.0 и выше может выполнять гораздо более сложные опе­ рации с достаточным количеством команд и используемых констант. Вы можете заранее выяснить возможности вашей графической кар­ ты, чтобы определить, какие версии она поддерживает. Зная теперь эти ограничения, будем создавать метод объединения дву­ мя способами. Первый использует очень простой код шейдера 1.1, под­ держиваемый практически всеми платами, второй — более сложный, для версии 2.0, с соответствующим ограничением на выбор платы. Естественно, мы начнем с версии 1.1, как с более простой. Перед на­ писанием соответствующего кода необходимо внести некоторые измене­ ния. В первую очередь нужно определить и создать вторую текстуру. Добавьте следующую переменную для этой текстуры в ваш класс:

private Texture skyTexture;

Часть III. Более совершенные методы построения графики Поскольку загружаемая нами модель имеет один материал и одну тек­ стуру, можно удалить ссылку на использование дополнительных масси­ вов материалов и текстур. Код, включенный в CD диск, уже включает в себя это изменение, поэтому на этом останавливаться не будем. Итак, мы создаем или загружаем вторую текстуру, чтобы затем объе­ динить ее с первой. В данном примере мы копируем текстуру skybox_top.JPG, которая находится в папке «application» DirectX SDK (можно в дальнейшем при желании использовать и другие текстуры). Как только мы выбрали вторую текстуру, необходимо ее создать. Наилучшим образом подойдет метод LoadMesh, которым была создана и первоначаль­ ная текстура. Исправьте строку создания текстуры: // We have a texture, try to load it meshTexture = TextureLoader.FromFile(device, @"..\..\" + mtrl[i].TextureFilename);

skyTexture = TextureLoader.FromFile(device, @"..\..\skybox_top.JPG");

Если вы выбрали другой файл текстуры, необходимо просто заменить имя файла в листинге. Теперь напишем код, позволяющий интерполиро­ вать имеющиеся текстуры. Добавьте следующие переменные к вашему коду шейдера: float Time;

Texture meshTexture;

Texture skyTexture;

sampler TextureSampler = sampler_state { texture = ;

mipfilter = LINEAR;

};

sampler SkyTextureSampler = sampler_state { texture = ;

mipfilter = LINEAR;

};

Переменная Time использовалась нами достаточно часто, поэтому не требует разъяснений, а вот две переменные текстуры являются новыми. Используемая здесь выборка текстур с помощью процедуры сэмплиро­ вания более предпочтительна, нежели вызов SetTexture, который мы ис­ пользовали раньше. По этой причине вызов SetTexture убирается из на­ шего приложения. Две новые переменные являются внутренними переменными языка шейдеров и служат для определения того, как текстура сэмплирована. Таким образом, каждая сэмплируемая текстура основывается на текущей текстуре и использует линейный фильтр mipmap. Мы бы могли также включить фильтры растяжения или сжатия, но для данного случая филь­ тра mipmap достаточно. Прежде чем использовать переменные текстурирования, необходимо определить их в программе инициализации основного кода, для этого добавьте следующий код после вызова загрузки mesh-объекта:

Глава 12. Использование языка шейдеров HLSL effect.SetValue("meshIexture", meshTexture);

effect.SetValue("skyTexture", skyTexture);

Таким образом, после загрузки mesh-объекта создаются наши тексту­ ры. Однако, даже после этого наша модель имеет только один единствен­ ный набор координат текстуры. Используйте следующий код вершинно­ го шейдера:

// Transform our coordinates into world space void TransformVl_l( in float4 inputPosition : POSITION, in float2 inputTexCoord : TEXCOORDO, out float4 outputPosition : POSITION, out float2 outputTexCoord : TEXCOORDO, out float2 outputSecondTexCoord : TEXC00RD1 ) { // Transform our position outputPosition = mul(inputPosition, WorldViewProj);

// Set our texture coordinates outputTexCoord = inputTexCoord;

outputSecondTexCoord = inputTexCoord;

} Понятно, что здесь вводятся местоположение и набор координат отдель­ ной текстуры. Однако, на выходе программа не просто возвращает преобра­ зованное местоположение текстуры, но также пересылает входную коорди­ нату текстуры к обоим наборам выходных координат. По существу, код дуб­ лирует координаты текстуры для каждой вершины поверхности. Теперь мы можем заменить код пиксельного шейдера на следующий:

void TextureColorVl_l ( in float4 P : POSITION, in float2 textureCoords : T X O R O EC OD, in float2 textureCoords2 : TEXC00RD1, out float4 diffuseColor : C L R ) OO O { // Get the texture color float4 diffuseColorl = tex2D(TextureSampler, textureCoords);

float4 diffuseColor2 = tex2D(SkyTextureSampler, textureCoords2);

diffuseColor = lerp(diffuseColorl, diffuseColor2, Time);

};

Здесь процедура принимает месторасположение и наборы координат текстур, возвращенные из подпрограммы вершинного шейдера, и выво­ дит цвет, которым должен быть отображен данный пиксел. В этом случае Часть III. Более совершенные методы построения графики мы сэмплируем каждую из двух загруженных текстур (с идентичными координатами текстур), а затем выполняем линейную интерполяцию (встроенная функция lerp). Значение переменной Time определяет то, как долго мы видим каждую текстуру. Пока мы не задавали данную перемен­ ную в нашем приложении. Можем сделать это в методе mesh drawing следующим образом:

effect.SetValue("Time", (float)Math.Abs(Math.Sin(angle)));

Напрашивается вопрос, почему мы выполняем «математику» именно здесь, а не в программе шейдера? Конечно, было бы предпочтительнее возложить данные операции на графическую карту, чем на центральный процессор, но мы об этом уже писали. Имеются в виду ограничения, свя­ занные с версией 1.1 используемого шейдера и нашей картой. Итак, перед запуском приложения с нашими текстурами осталось вы­ полнить одну замену. Поскольку имена функций для вершинного и пик­ сельного шейдеров были изменены, необходимо заменить используемую нами технику:

technique TransformTexture { pass РО { // shaders VertexShader = compile vs_l_l TransformVl_l();

PixelShader = compile ps_l_l TextureColorVl_l();

I Название самой техники не изменилось, и теперь мы можем запус­ тить наше приложение. Обратите внимание на то, что текстурируемая при запуске модель «смешивается» с текстурой неба Sky Texture (пола­ гая, что мы использовали эту текстуру) и затем снова объединяется с ис­ ходной текстурой. Это повторяется до тех пор, пока приложение не будет закрыто. Теперь предположим, что ваша графическая карта поддерживает пик­ сельный шейдер версии 2.0. Мы должны переписать код таким образом, чтобы иметь возможность работать с обеими версиями пиксельных шей­ деров 1.1 и 2.0. Чтобы узнать, поддерживает ли плата версию 2.0, необ­ ходимо добавить переменную к основному коду:

private bool canDo2_0Shaders = false;

Значение «false» предполагает, что плата не поддерживает версию 2.0 первоначально. После того как мы создали устройство в методе инициа Глава 12. Использование языка шейдеров HLSL лизации, необходимо выяснить, может ли шейдер 2.0 поддерживаться самим устройством. Этот вызов необходимо определить непосредствен­ но перед командой Effect:

canDo2_0Shaders = device.DeviceCaps.PixelShaderVersion >= new Version(2, 0);

Поскольку мы добавляем вторую технику для шейдера, необходимо, основываясь на более продвинутой модели шейдера, определить, какая именно техника будет являться базовой. Для этого перепишите запись effect.Technique: effect.Technique = canDo2JShaders ? "TransformTexture2_0" : "TransformTexture";

В этом случае, если модель 2.0 поддерживается, приложение будет использовать эту технику, в противном случае будет использоваться тех­ ника, которую мы уже написали. Теперь перепишем строку, которая оп­ ределяет переменную Time: if (canDo2_0Shaders) { effect.SetValue("Time", } else { angle);

effeet.SetValue("Time", (float)Math.Abs(Math.Sin (angle)));

} Обратите внимание, что с более совершенной программой шейдера и соответствующей графической картой выполнение математических опе­ раций будет возложено на графическую карту, и мы воспользуемся все• ми преимуществами данной конфигурации. Теперь нам необходимо до­ бавить новый код шейдера, который позволит нам выполнить приложе­ ние. Добавьте код, приведенный в листинге 12.1, к вашему коду HLSL.

Листинг 12.1. Процедура объединение текстур на языке шейдера версии 2.0. // Transform our coordinates void TransformV2_0( in float4 inputPosition : in float2 inputTexCoord : out floats outputPosition out float2 outputTexCoord ) ( into world space POSITION, TEXCOORDO, : POSITION, : TEXCOORDO Часть III. Более совершенные методы построения графики // Transform our position outputPosition = mul(inputPosition, WorldViewProj);

II Set our texture coordinates outputTexCoord = inputTexCoord;

) void TextureColorV2_0( in float4 P : POSITION, in float2 textureCoords : TEXCOORDO, out float4 diffuseColor : COLORO) ( // Get the texture color float4 diffuseColorl = tex2D(TextureSampler, textureCoords);

float4 diffuseColor2 = tex2D(SkyTextureSampler, textureCoords);

diffuseColor = lerp(diffuseColorl, diffuseColor2, abs(sin(Time)));

};

technique TransformTexture2_0 { pass PO { // shaders VertexShader = compile vs_l_l TransformV2_0();

PixelShader = compile ps_2_0 TextureColorV2_0() ;

} } Наша программа стала намного проще. Вместо дублирования коорди­ нат текстуры код просто преобразовывает местоположение и пересылает первоначальный набор координат. Нет ничего похожего на программы вершинных шейдеров (vertex program). Пиксельный шейдер вообще выглядит намного проще. Здесь нет не­ обходимости в использовании двух наборов координат текстур, вместо этого смешивание двух различных текстур происходит с помощью одно­ го и того же набора текстурных координат. В более старых версиях пик­ сельных шейдеров (до версии 2.0) мы могли читать координаты тексту­ ры только один раз, и процедура смешивания двух текстур с помощью одного набора координат вызвала бы два считывания, а это было невоз­ можно при наличии старой версии шейдера. Следует также заметить, что для определения уровня интерполяции (функция lerp) благодаря тому, что математика выполняется в коде шейдера, используется одна и та же формула. Единственные различия в данной технике — это имена функ­ ций и то, что пиксельный шейдер компилируется с помощью ps_2_0.

Текстуры освещения В предыдущей главе мы обсуждали простое направленное освеще­ ние. На том этапе цилиндр не текстурировался, и расчет освещения вы Глава 12. Использование языка шейдеров HLSL поднялся только вершинными шейдерами. Мы можем легко осуществить те же операции, используя пиксельные шейдеры. Подобно предыдущему примеру, мы выберем простой mesh-объект из предыдущей главы в качестве отправной точки. В этом разделе возьмем готовый пример и добавим цветное освещение к сцене в нашем пиксель­ ном шейдере. В качестве первого шага нам необходимо добавить некоторые объяв­ ления к коду шейдера:

float4x4 WorldMatrix : WORLD;

floats DiffuseDirection;

Texture meshTexture;

sampler TextureSampler = sampler^state { texture = ;

mipfilter = LINEAR;

};

struct VS_0UTPUT { float4 float2 float3 float3 Pos : POSITION;

TexCcord : TEXC0ORD3;

Light : TEXC00RD1;

Normal : TEXC00RD2;

};

Обратите внимание, что мы не просто сохраняем комбинированную матрицу вида и проекции как раньше, а имеется другая переменная для записи непосредственно мировой матрицы. Мы поймем необходимость этого, как только обсудим данный код шейдера. Также имеется перемен­ ная, отвечающая за направление света. Как и в предыдущем примере, имеется текстура и состояние сэмплера для чтения текстуры. Выходная структура данной процедуры несколько отличается. Пере­ сылаются не только местоположение и координаты текстуры, пересыла­ ются также два новых значения координат текстуры. В действительнос­ ти они не являются координатами текстуры как таковыми, они использу­ ются только для передачи необходимых данных из вершинного шейдера в пиксельный шейдер. Мы должны также переопределить эти значения в методе рисования объекта после вызова строки с переменной WorldViewProj:

effect.SetValue("WorldMatrix", worldMatrix);

effect.SetValue("DiffuseDirection", new Vector4(0, 0, 1, I));

А также устанавливаем переменную текстуры, добавляя в метод ини­ циализации следующий код после процедуры загрузки mesh-объекта (и текстуры):

effect.SetValue("meshTexture", meshTexture);

Часть III. Более совершенные методы построения графики Теперь все готово к тому, чтобы мы могли переписать наш код пик­ сельного шейдера и отобразить текстуру освещения. Осталось выбрать цвет освещения (любой кроме белого, для наглядности) и проверить вер­ шинный шейдер:

// Transform our coordinates into world space VS_OUTPUT Transform) float4 inputPosition : POSITION, float3 inputNormal : NORMAL, float2 inputTexCoord : TEXCOORDO //Declare our output structure VS_0UTPUT Out = (VS_OUTPUT)0;

// Transform our position Out.Pos = mul(inputPosition, WorldViewProj);

// Store our texture coordinates Out.TexCoord = inputTexCoord;

// Store our light direction Out.Light = DiffuseDirection;

// Transform the normals into the world matrix and normalize them Out.Normal = normalize(mul(inputNormal, WorldMatrix));

return Out;

} Начало этой программы шейдера для нас достаточно ясно, хотя в ней имеется значительное число входных параметров. Вначале преобразует­ ся местоположение и записываются координаты текстуры. Затем пара­ метр направления света «light direction» размещается во второй набор координат текстуры. Обратите внимание на то, что тип параметра на­ правления света — float4, тогда как тип координат текстуры — float3. В третьем наборе координат текстуры необходимо сохранить данные о нормалях. Прежде чем это сделать, необходимо преобразовать норма­ ли в мировую систему координат. Без этого расчет освещения будет вы­ полняться некорректно, нельзя выполнять математические операции над объектами в различных системах координат. Выполнив все необходимые действия с освещением и текстурами, мы можем добавить следующий код пиксельного шейдера:

floats TextureColor( float2 textureCoords : TEXCOORDO, float3 lightDirection : TEXC00RD1, float3 normal : TEXC00RD2) : COLORO { // Get the texture color float4 textureColor = tex2D(TextureSampler, textureCoords);

Глава 12. Использование языка шейдеров HLSL // Make our diffuse color purple for now float4 diffuseColor = (l.Of, O.Of, l.Of, l.Of);

// Return the combined color after calculating the effect of // the diffuse directional light return textureColor * (diffuseColor * saturate(dot(lightDirection, normal))) ;

};

Как вы видите, в качестве входных параметров программа пиксельно­ го шейдера использует все наборы координат текстуры, возвращенные программой вершинного шейдера, и определяет цвет этого пиксела. Пер­ вым шагом является выбор текущей текстуры. Затем устанавливается цвет освещения (в данном случае — фиолетовый). Чтобы рассчитать освеще­ ние в пикселе, необходимо взять скалярное произведение направления освещения и нормали, как это уже делалось раньше для вершинного шей­ дера. Встроенный метод saturate (насыщение) удерживает возвращаемое значение яркости в диапазоне от нуля до единицы, обеспечивая выравни­ вание чрезмерно ярких или чрезмерно темных цветов. Окончательно ре­ зультирующий цвет получается умножением текущего цвета текстуры на диффузные компоненты освещения. Несмотря на то, что этот пример чрезвычайно прост, он заслуживает внимания и является достаточно общим случаем. Запуская приложение, мы должны увидеть отображаемую модель с фиолетовым оттенком.

Моделирование световых бликов Другим типом освещения, который мы еще не обсуждали, является «specular highlighting» или моделирование световых бликов на поверх­ ности. Данный метод позволяет отобразить объект на экране более реа­ листично. Мы можем реализовать данный тип освещения с помощью непрограммируемого конвейера, однако при этом эти подсветки будут рассчитаны повершинно. В этой главе мы покажем, как можно реализо­ вать световые блики, используя программируемый конвейер. Мы будем опираться на более раннюю секцию кода и за основу возьмем пример кода для освещенного цилиндра, написанный в предыдущей гла­ ве. Данный код использует метод для случая диффузного освещения, его мы оставим для сравнения, добавив соответствующий алгоритм для но­ вой модели освещения. Для большей наглядности мы выберем вместо цилиндра объект teapot. Для этого перепишите код загрузки цилиндра, заменив название загру­ жаемого объекта:

// Load our mesh mesh = Mesh.Teapot(device);

Часть III. Более совершенные методы построения графики Поскольку этот пример будет демонстрировать различные эффекты, следует позаботиться о возможности переключения между ними, и, кро­ ме того, было бы неплохо добавлять соответствующие текстовые ком­ ментарии. Поэтому необходимо определить переменную шрифта: // Out font private Direct3D.Font font = null;

Инициализируем переменную шрифта сразу после создания объекта teapot: //Create our font font = new Direct3D.Font(device, new System.Drawing.Font("Arial", 12.Of));

Теперь необходимо добавить константы и переменные, которые будут задействованы в коде пиксельного шейдера: float4x4 WorldViewProj : W R D I W R J C I N O L VE P OE TO ;

float4x4 WorldMatrix : W R D OL ;

float4 DiffuseDirection;

float4 EyeLocation;

// Color constants const float4 MetallicColor = { 0.8f, 0.8f, 0.8f, l.Of };

const float4 AmbientColor = { 0.05f, 0.05f, 0.05f, l.Of };

Для преобразования вершин здесь используются мировая матрица, мат­ рица вида и матрица проекции. Мировая матрица будет еще раз использова­ на для преобразования нормалей к вершинам. Значение DiffuseDirection по­ зволит приложению определять направление света быстрее, чем при исполь­ зовании шейдера в предыдущих примерах. И последняя переменная EyeLocation — положение глаза наблюдателя. Световые блики рассчитыва­ ются, исходя из отражения света между нормалью к поверхности и глазом. Обратите внимание на то, что в нашей подпрограмме имеются две объявленные константы. Цвет MetallicColor выбирается, благодаря свой­ ствам металлической поверхности и падающего на него диффузного ос­ вещения. Вторая объявленная константа — общее освещение AmbientColor. Данный параметр включен для полноты математических опера­ ций при расчете освещения. Для данного примера выходная структура повершинного освещения per-vertex рассматривает и задает местоположение и цвет каждой верши­ ны. Мы можем объявить ее следующим образом:

s t r u c t VS_OOTPUT_PER_VERTEX float4 Position : POSITION;

float4 Color : COLORO;

I;

Глава 12. Использование языка шейдеров HLSL Прежде чем написать код шейдера для моделирования световых бли­ ков, необходимо переписать код шейдера для диффузного освещения. Таким образом мы создадим отдельные шейдеры для каждой свето­ вой модели. Замените программу для диффузного освещения следую­ щим кодом:

VS_OUTPUT_PER_VERTEX TransformDiffuse( floats inputPosition : POSITION, float3 inputNormal : NORMAL, uniform bool metallic ) { //Declare our output structure VS_0UTPUT_PER_VERTEX Out = (VS_OUIPUT_PER_VERTEX)0;

// Transform our position Out.Position = mul(inputPosition, WorldViewProj);

// Transform the normals into the world matrix and normalize them float3 Normal = normalize(mul(inputNormal, WorldMatrix));

// Make our diffuse color metallic for now float4 diffuseColor = MetallicColor;

if(!metallic) diffuseColor.rgb = sin(Normal + inputPosition);

// Store our diffuse component float4 diffuse = saturate(dot(DiffuseDirection, Normal));

// Return the combined color Out.Color = AmbientColor + diffuseColor * diffuse;

return Out;

} Обратите внимание, что в шейдере появился новый входной параметр, булева переменная, объявленная с атрибутом «uniform». Модификатор «uniform» позволяет приложению Direct3D обработать эту переменную как константу, которая не может быть изменена в течение вызова рисова­ ния draw. При запуске данного шейдера преобразуется местоположение и затем нормаль, а для диффузного освещения выбирается объявленный ранее цвет «metallic». Также присутствует оператор, который мы еще не рассматривали, а именно, управление потоком данных (flow control). Язык HLSL под­ держивает несколько механизмов управления потоками данных, вклю­ чая знакомые нам условные операторы «if», операторы циклов, преры­ вание цикла «while loop» и т. д. Каждый из механизмов управления по­ токами данных имеет свой синтаксис, подобный эквивалентным опера­ торам в С#.

Часть III. Более совершенные методы построения графики УПРАВЛЕНИЕ ПОТОКАМИ ДАННЫХ НА СТАРЫХ ВЕРСИЯХ ШЕЙДЕРОВ Более старые версии шейдеров не поддерживают управление по­ токами данных или переходы. Компилятор HLSL может раскручивать цикл или выполнять все вет­ ви условных операторов. Это необходимо знать, чтобы при написа­ нии сложных шейдеров не достичь предельного количества опера­ торов. Возьмем простой цикл:

for(int i = 0;

i<10;

i++) { pos.y += (float)i;

} Даже при том, что здесь имеется только один оператор, данный код шейдера инициировал бы по крайней мере выполнение 20 команд. И если бы шейдер имел ограничение в 12 команд (например, шейдер версии 1.1), этот простой цикл не был бы откомпилирован. Итак, вернемся к нашей программе. Если переменная metallic истинна, то сохраняется металлический цвет освещения. Если ложное, цвет будет переключен на цвет «animating», аналогично первому примеру в этой главе. Теперь, имея два различных типа цвета, которые могут использовать­ ся в этой программе шейдера, мы добавляем две техники:

technique TransformSpecularPerVertexMetallic { pass PO { // shaders VertexShader = compile vs_l_l TransformSpecular(true) ;

PixelShader = NULL;

} } technique TransformSpecularPerVertexColorful { pass PO { // shaders VertexShader = compile vs_l_l TransformSpecular(false);

PixelShader = NULL;

} } Глава 12. Использование языка шейдеров HLSL Следует обратить внимание, что единственное различие между этими двумя техниками (помимо разных названий) — значение, которое пере­ сылается в код шейдера. Далее, поскольку названия используемых техник изменились, необ­ ходимо переписать основной код:

effect.Technique = "TransformDiffusePerVertexMetallic";

Далее необходимо добавить новую программу шейдера для модели­ рования световых бликов с учетом диффузного освещения и использова­ ния переходов за область оператора в программе. Добавьте метод, приве­ денный в листинге 12.2, к вашему коду HLSL.

Листинг 12.2. Преобразования и использование световых бликов. VS_OUTPUT_PER_VERTEX TransformSpecular ( float4 inputPosition : POSITION, float3 inputNormal : NORMAL, uniform bool metallic ) { ( //Declare our output structure VS_OUTPUT_PER_VERTEX Out = (VS_OUTPUT_PER_VERTEX)0;

// Transform our position Out.Position = mul(inputPosition, WorldViewProj);

// Transform the normals into the world matrix and normalize them float3 Normal = normalize(mul(inputNormal, WorldMatrix));

// Make our diffuse color metallic for now float4 diffuseColor = MetallicColor;

// Normalize the world position of the vertex float3 worldPosition = normalize(mul(inputPosition, WorldMatrix));

// Store the eye vector float3 eye = EyeLocation - worldPosition;

// Normalize our vectors float3 normal = normalize(Normal);

float3 light = normalize(DiffuseDirectipn);

float3 eyeDirection = normalize(eye);

if([metallic) diffuseColor.rgb = cos(normal + eye);

// Store our diffuse component float4 diffuse = saturate(dot(light, normal));

// Calculate specular component float3 reflection = normalize(2 * diffuse * normal - light);

floats specular = pow(saturate(dot(reflection, eyeDirection)), 8);

Часть III. Более совершенные методы построения графики // Return the combined color Out.Color = AmbientColor + diffuseColor * diffuse + specular;

return Out;

} Это самая длинная программа шейдера в этой книге из рассмотрен­ ных до настоящего момента. Начало аналогично предыдущему случаю, когда мы преобразовывали местоположение и нормаль и затем устанав­ ливали диффузное освещение для константы цвета «metallic». После этого в программе сохраняется местоположение каждой вершины в мировых координатах, поскольку наблюдатель расположен в пространстве миро­ вых координат, а вершины — в пространстве модели. И, поскольку при вычислении световых бликов будет использоваться направление от на­ блюдателя, все значения должны находиться в одной системе координат. Далее каждый из векторов нормализуется к значению l.Of, после чего проверяется булева переменная, и пересчитывается значение цвета с уче­ том нового нормализованного вектора и сохраненного диффузного осве­ щения, входящих в формулу расчета, таким образом, моделируется слу­ чайный цвет анимации и окончательно рассчитывается модель световых бликов. Для получения дополнительной информации по формуле расче­ та освещения см. документацию DirectX SDK. После того как рассчитаны компоненты световой модели, программа шейдера возвращает результирующий цвет, используя те же самые мате­ матические формулы для освещения. После того как мы написали тен­ деры для моделирования световых бликов, можно добавить следующие процедуры обращения к ним:

technique TransformSpecularPerVertexMetallic { pass PO { // shaders VertexShader = compile vsl_l TransformSpecular(true);

PixelShader = NULL;

technique TransformSpecularPerVertexColorful { pass PO { // shaders VertexShader = compile vs_l_l TransformSpecular(false);

PixelShader = NULL;

} } } } Глава 12. Использование языка шейдеров HLSL Перед запуском приложения необходимо переписать основной код, чтобы вызвать программу шейдера для моделирования световых бликов: effect.Technique = "TransformSpecularPerVertexMetallic";

На рис.12.1 изображен вращающийся, освещенный светом чайник.

Рис. 12.1 Модель чайника использующая повершииное (per-vertex) моделирование световых бликов ИСПОЛЬЗОВАНИЕ ПОПИКСЕЛЬНОГО (PER-PIXEL) МОДЕЛИРОВАНИЯ СВЕТОВЫХ БЛИКОВ Легко заметить, что освещенный солнцем чайник выглядит гораздо реалистичнее, если мы используем световые блики. Тем не менее, может показаться, что повершинное освещение, рассчитываемое в этой программе, не так гладко освещает изогнутую поверхность заварочного чайника, как хотелось бы. Чтобы получить более мяг­ кую игру света, можно рассчитать освещение, используя попиксельное моделирование освещения. Поскольку вычисление освещения требует большего количества ко­ манд, чем позволяет пиксельный шейдер версии 1.1, необходимо убедиться, что ваше приложение поддерживает по крайней мере шейдеры версии не ниже 2.0. Чтобы проверить эту «логику» пере­ пишите основной код:

Часть III. Более совершенные методы построения графики if ((hardware.VertexShaderVersion >= new Version(1, 1)) && (hardware.PixelShaderVersion >= new Version(2, 0))) Нам все еще понадобится вершинный шейдер для преобразования координат и передачи данных, которые будут использоваться в пик­ сельном шейдере. Теперь добавьте программу шейдера, приведен­ ную в листинге 12.3.

Листинг 12.3. Программа шейдера для моделирования световых бликов. struct VS_OUTPUT_PER_VERTEX_PER_PIXEL { float4 Position : POSITION;

float3 LightDirection : TEXCOORDO;

float3 Normal : TEXCOORD1;

float3 EyeWorld : TEXCOORD2;

};

// Transform our coordinates into world space VS_OUTPUT_PER_VERTEX_PER_PIXEL Transform( float4 inputPosition : POSITION, float3 inputNoraal : NORMAL ) { //Declare our output structure VS_OUTPUT_PER_VERTEX_PER_PIXEL Out = (VS_OUTPUT_PER_VERTEX_PER_PIXEL) // Transform our position Out.Position = mul(inputPosition, WorldViewProj);

// Store our light direction Out.LightDirection = DiffuseDirection;

// Transform the normals into the world matrix and normalize them Out.Normal = normalize(mul(inputNormal, WorldMatrix));

// Normalize the world position of the vertex float3 worldPosition = normalize(mul(inputPosition, WorldMatrix));

// Store the eye vector Out.EyeWorld = EyeLocation - worldPosition;

return Out;

} float4 ColorSpecular( float3 lightDirection : TEXCOORDO, float3 normal : TEXC00RD1, float3 eye : TEXC00RD2, uniform bool metallic) : COLORO Глава 12. Использование языка шейдеров HLSL // Make our diffuse color metallic for now floats diffuseColor = MetallicColor;

if(!metallic) diffuseColor.rgb = cos(normal + eye);

// Normalize our vectors float3 normalized = normalize(normal);

float3 light = normalize(lightDirection);

float3 eyeDirection = normalize(eye);

// Store our diffuse component float4 diffuse = saturate(dot(light, normalized));

// Calculate specular component float3 reflection = normalize(2 * diffuse * normalized- light);

float4 specular = pow(saturate(dot(reflection, eyeDirection)), 8);

// Return the combined color return AmbientColor + diffuseColor * diffuse + specular;

};

Написанная программа напоминает код для повершинной обработ­ ки света. Вершинный шейдер выполняет преобразования и сохра­ няет необходимые данные для последующей программы пиксель­ ного шейдера, которая в свою очередь выполняет все математи­ ческие операции для определения конечного цвета. Поскольку попиксельные операции выполняются быстрее чем повершинные, полученная в результате картинка будет более мягкой и реалистич­ ной. Добавьте следующую методику к вашему файлу шейдера: technique TransformSpecularPerPixelMetallic { pass РО { // shaders VertexShader = compile vs_l_l Transform!);

PixelShader = compile ps_2_0 ColorSpecular(true);

} } Переключите приложение, чтобы можно было сравнить результаты использования двух методик. На рис.12.2 изображена более реалистичная картинка вращающе­ гося освещенного чайника.

Часть III. Более совершенные методы построения графики Рис. 12.2. Модель чайника, использующая попиксельное (per-pixel) моделирование световых бликов Пример, включенный в CD диск, позволяет плавное переключение между попиксельным и повершинным исполнением программы, а также позволяет переключать между металлическим и полноцветным освеще­ нием поверхности.

Краткие выводы В этой главе мы рассмотрели следующие вопросы. • Простая анимация вершины и цветная анимация. • Объединение цвета текстуры с цветами поверхности. • Более совершенные модели освещения. В следующей мы обсудим создание анимации в Управляемом DirectX Глава 13. Рендеринг скелетной анимации Глава 13. Рендеринг скелетной анимации Как правило, в современных играх мы наблюдаем непрерывные сгла­ женные динамические перемещения объектов. Элементы полностью анимированы, при анимации используется система захвата движения объекта (motion capture studio). Скелетная анимация используется при моделировании различных движений объекта или его частей. Помимо сложных мультипликаций имеются также другие, более простые типы анимации, имеющие дело главным образом с масштабированием, вра­ щением и перемещением. В этой главе мы рассмотрим рендеринг meshобъектов с анимацией данных, включая. • Загрузку иерархической системы фреймов. • Формирование скелетных данных. Рендеринг каждого фрейма. • Оптимизацию времени анимации. Использование индексированной анимации для улучшения быст­ родействия.

Создание иерархической системы фреймов Большинство mesh-объектов (даже не имеющих встроенной анима­ ции) обладают некоторой иерархией: рука, приложенная к груди, палец, приложенный к руке и т. д. Данные иерархии можно загружать и удержи­ вать с помощью библиотеки расширений Direct3D. Чтобы сохранять эту информацию, существует два абстрактных клас­ са, которые поддерживают иерархию: класс Frame и класс MeshContainer. Каждый фрейм может содержать родственные или дочерние фреймы. Каждый фрейм может также содержать нулевой или свободный указа­ тель zero к контейнерам класса MeshContainer. Как обычно, начнем написание программы для Direct3D с создания нового проекта, определения переменных, окон и т. п. Объявите следую­ щие переменные в приложении:

private private private private AnimationRootFrame rootFrame;

Vector3 objectCenter;

float objectRadius;

float elapsedTime;

Структура AnimationRootFrame содержит корневой фрейм дерева клас­ са анимации. Следующие две переменные будут содержать центр загру­ жаемого объекта mesh и радиус сферы, ограничивающей объект. Это по­ зволит позиционировать камеру таким образом, чтобы захватывать всю 9 Зак. Часть III. Более совершенные методы построения графики модель. Последняя переменная определяет время выполнения, которое будет использоваться для обновления анимации. Поскольку различные объекты анимируются по-разному, необходимо создать новый производный класс из объекта иерархий AllocateHierarchy. Для этого добавьте к приложению следующий класс:

public class AllocateHierarchyDerived : AllocateHierarchy { Forml app = null;

///

/// Create new instance of this class /// /// Parent of this class public AllocateHierarchyDerived(Forml parent) { app = parent;

} } Пока данный класс не задействован. Конструктор программ сохраня­ ет его основную форму так, чтобы в дальнейшем использовать данный класс для вызова соответствующих методов, но на данном этапе прило­ жение не будет его даже компилировать. Имеются два абстрактных мето­ да, которые должны быть реализованы в этом классе еще до компиля­ ции. Эти два метода используются для создания фреймов и контейнеров MeshContainer, позволяющих сохранить иерархию. Поскольку оба мето­ да помечены как абстрактные, они не могут быть созданы автоматичес­ ки, поэтому необходимо добавить новые версии производных классов к вашему приложению:

public class FrameDerived : Frame { // Store the combined transformation matrix private Matrix combined = Matrix.Identity;

public Matrix CombinedTransformationMatrix { get { return combined;

} set { combined = value;

} } } Таким образом, в дополнении к данным нормали, записанным во фрей­ ме, каждый фрейм будет сохранять матрицу преобразований, скомбиниро­ ванную из данных фрейма и данных всех его «родителей». Это может при­ годиться в дальнейшем при формировании мировых матриц. Теперь необ­ ходимо включить производный класс mesh container, см. листинг 13.1.

Глава 13. Рендеринг скелетной анимации Листинг 13.1. Добавление класса MeshContainer.

public class MeshContainerDerived : MeshContainer { private Texture[] meshTextures = null;

private int numAttr = 0;

private int numlnfl = 0;

private BoneCombination[] bones;

private FrameDerived[] frameMatrices;

private Matrix[] offsetMatrices;

// Public properties public Texture[] GetTextures() { return meshTextures;

} public void SetTextures(Texture[] textures) { meshTextures = textures;

} public BoneCombination[] GetBones() { return bones;

} public void SetBones(BoneCombination[] b) { bones = b;

} public FrameDerived[] GetFrames() { return frameMatrices;

} public void SetFrames(FrameDerived[] frames) { frameMatrices = frames;

} public Matrix[] GetOffsetMatrices() { return offsetMatrices;

) public void SetoffsetMatrices(Matrix[] matrices){offsetMatrices = matrices;

} public int NumberAttributes {get{return numAttr;

} set{numAttr = value;

}} public int Numberlnfluences {get{return numInfl;

}set{numInfl = value;

}} } Каждый контейнер MeshContainer будет содержать информацию, вклю­ чающую таблицу комбинаций каркасов, номера атрибутов, данные о мат­ рицах влияний и матрицах смещения. Каждый параметр этих данных будет использоваться при рендеринге нашего анимационного объекта. Создав необходимые производные классы, можно добавить абстракт­ ные методы. Вначале добавьте перегрузку CreateFrame к производному классу allocate hierarchy:

public override Frame CreateFrame(string name) { FrameDerived frame = new FrameDerived();

frame. Name = name;

frame.TransformationMatrix = Matrix.Identity;

frame.CombinedTransformationMatrix = Matrix.Identity;

return frame;

} Данный код записывает название фрейма, объявляет матрицы преоб­ разования и т. д. Родственные и дочерние фреймы, а также контейнеры MeshContainer будут заполняться автоматически за время выполнения приложения.

Часть III. Более совершенные методы построения графики Для создания контейнера MeshContainer добавьте код, приведенный в листинге 13.2.

Листинг 13.2. Создание раздела MeshContainer. public override MeshContainer CreateMeshContainer(string name, MeshData meshData, ExtendedMaterial[] materials, Effectlnstance effectlnstances, GraphicsStream adjacency, Skinlnformation skinlnfo) { // We only handle meshes here if (meshData.Mesh == null) throw new ArgumentException();

// We must have a vertex format mesh if (meshData.Mesh.VertexFormat == VertexFormats.None) throw new ArgumentException();

MeshContainerDerived mesh = new MeshContainerDerived();

mesh. Name = name;

int numFaces = meshData.Mesh.NumberFaces;

Device dev = meshData.Mesh.Device;

// Make sure there are normals if ((meshData.Mesh.VertexFormat & VertexFormats.Normal) == 0) { // Clone the mesh Mesh tempMesh = meshData.Mesh.Clone(meshData.Mesh.Options.Value, meshData.Mesh.VertexFormat | VertexFormats.Normal, dev);

meshData.Mesh = tempMesh;

meshData.Mesh.ComputeNormals();

} // Store the materials mesh.SetMaterials(materials) ;

mesh.SetAdjacency(adjacency);

Texture[] meshTextures = new Texture[materials.Length];

// Create any textures for (int i = 0;

i < materials.Length;

i++) { if (materials[i].TextureFilename != null) { meshTextures[i] = TextureLoader.FromFile(dev, @"..\..\" + materials[i].TextureFilename);

} } mesh.SetTextures(meshTextures);

mesh.MeshData = meshData;

// If there is skinning info, save any required data if (skinlnfo != null) Глава 13. Рендеринг скелетной анимации mesh.Skinlnformation = skinlnfo;

int numBones = skinlnfo.NumberBones;

Matrix[] offsetMatrices = new Matrix[numBones];

for (int i = 0;

i < numBones;

i++) offsetMatrices[i] = skinlnfo.GetBoneOffsetMatrix(i);

mesh.SetOffsetMatrices(offsetMatrices) ;

app.GenerateSkinnedMesh(mesh);

} return mesh;

} Это выглядит несколько сложнее, чем на самом деле. В первую оче­ редь проверяется, поддерживает ли приложение данный объект. Очевид­ но, что при отсутствии объекта, уже включенного в структуру данных, параметр будет исключен (строка throw new ArgumentException). To же самое произойдет, если включенный в структуру объект не использует допустимый вершинный формат. В случае, если данные объекта mesh достоверны, создается новый контейнер MeshContainer, куда наряду с устройством записываются имя и число поверхностей объекта. Код может также использовать дополни­ тельную индивидуальную переменную для доступа к винформам уст­ ройства. Далее идет более-менее знакомый нам фрагмент кода — проверяются данные нормали для объекта. Если данные отсутствуют, программа до­ бавляет их к объекту. Затем сохраняются материалы и информация смеж­ ности «adjacency», а также создается массив данных текстур заданных материалов. Далее записывается соответствующая текстура в каждый член массива, который затем сохраняется в контейнере MeshContainer. В конце проверяется наличие скелетной информации в контейнере MeshContainer, после чего информация сохраняется, и формируется мас• сив из матриц смещения (для каждого каркаса). После этого из нашей формы вызывается пока еще не существующий метод GenerateSkinnedMesh. По этой причине приложение все еще не будет компилировать про­ грамму, необходимо добавить метод GenerateSkinnedMesh, приведенный в листинге 13.3, в наш основной класс. Листинг 13.3. Создание каркасных объектов Skinned Meshes. public void GenerateSkinnedMesh(MeshContainerDerived mesh) { if (mesh.SkinInformation == null) throw new ArgumentException() ;

int numlnfl = 0;

Часть III. Более совершенные методы построения графики BoneCombination[] bones;

// Use ConvertToBlendedMesh to generate a drawable mesh MeshData m = mesh.MeshData;

m.Mesh = mesh.Skinlnformation.ConvertToBlendedMesh(m.Mesh, MeshFlags.Managed | MeshFlags.OptimizeVertexCache, mesh.GetAdjacencyStream(), out numlnfl, out bones);

// Store this info mesh.Numberlnfluences = numlnfl;

mesh.SetBones (bones);

// Get the number of attributes mesh.NumberAttributes = bones.Length;

mesh.MeshData = m;

} Данный метод никогда не вызывается, если не имеется никакой ске­ летной информации. Мы временно сохраняем данные объекта и исполь­ зуем метод ConvertToBlendedMesh, чтобы создать новый объект с повершинной комбинацией весов и таблицей комбинаций каркасных элемен­ тов. Это — «корневой» метод для анимирования объекта. Наконец, мы сохраняем набор связей в этом объекте, таблицу комбинаций каркасных элементов и атрибуты.

Загрузка объектов с анимацией Итак, в предыдущем параграфе была установлена основная иерархия объекта. Теперь нам необходимо записать некоторый код, чтобы факти­ чески использовать ее. Как обычно, нам нужно инициализировать гра­ фическое устройство, используя метод, аналогичный используемым нами ранее в программируемом конвейере, см. листинг 13.4.

Листинг 13.4. Инициализация графики. public bool InitializeGraphics() { // Set our presentation parameters PresentParameters presentParams = new PresentParameters();

presentParams.Windowed = true;

presentParams.SwapEffeet = SwapEffeet.Discard;

presentParams.AutoDepthStencilFormat = DepthFormat.D16;

presentParams.EnableAutoDepthStencil = true;

bool canDoHardwareSkinning = true;

// Does a hardware device support shaders? Caps hardware = Manager.GetDeviceCaps(O, DeviceType.Hardware);

// We will need at least four blend matrices if (hardware.MaxVertexBlendMatrices >= 4) Глава 13. Рендеринг скелетной анимации // Default to software processing CreateFlags flags = CreateFlags.SoftwareVertexProcessing;

// Use hardware if it's available if (hardware.DeviceCaps.SupportsHardwareTransformAndLight) flags = CreateFlags.HardwareVertexProcessing;

// Use pure if it's available if (hardware.DeviceCaps.SupportsPureDevice) flags != CreateFlags.PureDevice;

// Yes, Create our device device = new Device(0, DeviceType.Hardware, this, flags, presentParams);

} else { / / N o shader support canDoHardwareSkinning = false;

// Create a reference device device = new Device(0, DeviceType.Reference, this, CreateFlags.SoftwareVertexProcessing, presentParams);

} // Create the animation CreateAnimation(@".,\..\tiny.x", presentParams);

// Hook the device reset event device.DeviceReset += new EventHandler(OnDeviceReset);

OnDeviceReset(device, null);

return canDoHardwareSkinning;

} Программа проверяет аппаратные возможности и при необходимости эмулирует программное устройство, поскольку файл анимации, исполь­ зуемый для данного примера, потребует по крайней мере четыре сме­ шанные матрицы вершин. Как только мы создали устройство, необходимо, определив соответ­ ствующий метод, загрузить анимацию. Перед добавлением метода заг­ рузки анимации нам следует написать обработчик события сброса уст­ ройства для этого метода. Код обработчика событий, который необходи­ мо добавить в наш класс, приведен в листинге 13.5.

Листинг 13.5. Обработчик события сброса устройства. private void OnDeviceReset(object sender, EventArgs e) { Device dev = (Device)sender;

// Set the view matrix Vector3 vEye = new Vector3( 0, 0, -1.8f * objectRadius );

Vector3 vUp = new Vector3( 0, 1, 0 );

Часть III. Более совершенные методы построения графики dev.Transform.View = Matrix.LookAtLH(vEye, objectCenter, vUp);

// Setup the projection matrix float aspectRatio = (float)dev.PresentationParameters.BackBufferWidth / (float)dev.PresentationParameters.BackBufferHeight, dev.Transform.Projection = Matrix.PerspectiveFovLH( (float)Math.PI / 4, aspectRatio, objectRadius/64.0f, objectRadius*200.0f );

// Initialize our light dev.Lights[0].Type = LightType.Directional;

dev.Lights[0].Direction = new Vector3(0.0f, O.Of, l.Of);

dev.Lights[0].Diffuse = Color.White;

dev.Lights[0].Commit () ;

dev.Lights[0].Enabled = true;

} Как вы можете видеть, в программе устанавливается матрица вида;

" камера размещается достаточно далеко, чтобы был виден весь объект, и направляется на центр объекта. Матрица проекции формируется с уче­ том достаточно большой задней плоскости, чтобы также вместить всю модель. Также имеется один направленный источник освещения. Теперь все готово для создания анимации, добавьте следующий код:

private void CreateAnimation(string file, PresentParameters presentParams) { // Create our allocate hierarchy derived class AllocateHierarchyDerived alloc = new AllocateHierarchyDerived(this);

// Load our file rootFrame = Mesh.LoadHierarchyFromFile(file, MeshFlags.Managed, device, alloc, null);

// Calculate the center and radius of a bounding sphere obj ectRadius = Frame.CalculateBoundingSphere(rootFrame.FrameHierarchy, out objectCenter);

// Setup the matrices for animation SetupBoneMatrices((FrameDerived)rootFrame.FrameHierarchy);

// Start the timer DXUtil.Timer(DirectXTimer.Start) ;

} Метод кажется довольно простым, но в действительности он вызыва­ ет другие методы. Сначала реализуется производный класс allocate hierarchy. Затем вызыва­ ется метод LoadffierarchyFromFile. При вызове этого метода следует учесть, что перегрузки CreateFrame и CreateMeshContainer вызываются неоднократ­ но, в зависимости от числа фреймов и контейнеров MeshContainer в вашем объекте. Возвращаемый объект AnimationRootFrame будет включать корне­ вой фрейм иерархичного дерева и контроллер анимации для этого объекта.

Глава 13. Рендеринг скелетной анимации После создания иерархии рассчитывается граничная сфера всего фрей­ ма (метод CalculateBoundingSphere). Метод CalculateBoundingSphere воз­ вращает радиус этой сферы и центр объекта (который уже использовался для установки камеры). И, последнее, создаются матрицы каркасов (Bone Matrices). Это пер­ вый метод, который будет обходить дерево иерархии фрейма, и он же будет базовым для остальных методов, приведенных в листинге 13.6.

Листинг 13.6. Создание матриц каркасов. private void SetupBoneMatrices(FrameDerived frame) { if (frame.MeshContainer != null) { SetupBoneMatrices((MeshContainerDerived)frame.MeshContainer);

} if (frame.FrameSibling != null) { SetupBoneMatrices((FrameDerived)frame.FrameSibling) ;

} if (frame.FrameFirstChild != null) { SetupBoneMatrices((FrameDerived)frame.FrameFirstChild);

} } private void SetupBoneMatrices(MeshContainerDerived mesh) { // Is there skin information? If so, setup the matrices if (mesh.Skinlnformation != null) { int numBones = mesh.Skinlnformation.NumberBones;

FrameDerived[] frameMatrices = new FrameDerived[numBones];

for (int i = 0;

i< numBones;

i++) { FrameDerived frame = (FrameDerived)Frame.Find( rootFrame.FrameHierarchy, mesh.Skinlnformation.GetBoneName(i)) ;

if (frame == null) throw new ArgumentException();

frameMatrices [i] = frame;

} mesh.SetFrames(frameMatrices);

} } Часть III. Более совершенные методы построения графики Как вы можете видеть, обход иерархии не представляется сложным. Если имеется родственный фрейм, вызывается непосредственно метод, в котором мы в настоящее время находимся. То же самое происходит, если имеются дочерние фреймы. Следует отметить, что при дочерних фрей­ мах вызывать метод из самого себя целесообразно только один раз, по­ скольку последующие дочерние фреймы будут друг для друга родствен­ ными. В этом случае мы сохраняем каждый фрейм в соответствии с на­ званием его каркаса. Перегрузка, которая принимает фрейм, не представляет интереса, ее единственное назначение заключается в обходе иерархичного дерева и передаче контейнера MeshContainer во вторую перегрузку. Таким обра­ зом, мы создали массив фреймов (для каждого каркаса объекта), описы­ вающий скелетную информацию, которая понадобится нам в дальней­ шем. ИСПОЛЬЗОВАНИЕ ТАЙМЕРА DIRECTX Для системы анимации понадобится использование высокоточно­ го таймера. Написанный нами код уже подразумевает использова­ ние таймера DirectX, который включен в SDK. Мы можем добавить этот файл к проекту, щелкнув по папке Add Existing Item и выбрав исходный файл утилиты dxutil.cs. Теперь настало время переписать наш основной метод с учетом ини­ циализации нашей новой графики, см. листинг 13.7. Листинг 13.7. Основная точка входа. static void Main() { using (Forml frm = new Forml()) { // Show our form and initialize our graphics engine frm.Show();

if (!frm.InitializeGraphics()) { MessageBox.Show("Your card can not perform skeletal animation on " + "this file in hardware. This application will run in " + "reference mode instead.");

} Application.Run(frm);

} } Глава 13. Рендеринг скелетной анимации Рендеринг анимированных объектов Теперь нам осталось отобразить нашу анимацию на экране. Метод рендеринга сам по себе кажется обманчиво простым, см. добавленный к нашему классу код в листинге 13.8.

Листинг 13.8. Рендеринг анимированного бъекта. protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { ProcessNextFrame();

device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.CornflowerBlue, l.Of, 0);

device.BeginScene(), // Draw our root frame DrawFrame((FrameDerived)rootFrame.FrameHierarchy);

device. EndScene() ;

device.Present();

this. Invalidated();

} Далее мы объявляем метод обработки фрейма «ProcessNextFrame», очищаем устройство и рисуем корневой фрейм:

private void ProcessNextFrame() { // Get the current elapsed time elapsedTime = DXUtil.Timer(DirectXTimer.GetElapsedTime);

// Set the world matrix Matrix worldMatrix = Matrix.Translation(objectCenter) ;

device.Transform.World = worldMatrix;

if (rootFrame.AnimationController != null) rootFrame.AnimationController.AdvanceTime(elapsedTime, null);

UpdateFrameMatrices((FrameDerived)rootFrame.FrameHierarchy, worldMatrix);

} Вначале сохраняется текущее время исполнения, параметр elapsed time. Затем создается мировая матрица для корневого фрейма (в прин­ ципе, это матрица перемещения в центр объекта, с последующим об­ новлением устройства). Полагая, что объект можно анимировать, мы устанавливаем временной параметр advance time, используя сохранен­ ный параметр elapsed time. Теперь необходимо перезаписать матрицы преобразования:

Часть III. Более совершенные методы построения графики private void UpdateFrameMatrices(FrameDerived frame, Matrix parentMatrix) { frame.CombinedTransformationMatrix = frame.TransformationMatrix * parentMatrix;

if (frame.FrameSibling != null) { UpdateFrameMatrices((FrameDerived)frame.FrameSibling, parentMatrix);

} if (frame.FrameFirstChild != null) { UpdateFrameMatrices((FrameDerived)frame.FrameFirstChild, frame.CombinedTransformationMatrix);

} } В этом методе общая матрица преобразования текущего фрейма по­ лучена путем умножения матрицы преобразования имеющегося фрейма на родительскую матрицу преобразования. Каждый из родственных фрей­ мов использует соответствующую родительскую матрицу текущего фрей­ ма. Каждый из дочерних фреймов должен использовать общую матрицу преобразования текущих фреймов, чтобы объединить ее с собственной. Таким образом, формируется цепочка матриц, где окончательный дочер­ ний фрейм имеет собственную матрицу преобразования, объединенную с каждой родительской матрицей. Теперь мы можем нарисовать наш объект, используя знакомый нам метод рисования DrawFrame:

private void DrawFrame(FrameDerived frame) { MeshContainerDerived mesh = (MeshContainerDerived)frame.MeshContainer;

while(mesh != null) { DrawMeshContainer(mesh, frame);

mesh = (MeshContainerDerived)mesh.NextContainer;

} if (frame.FrameSibling != null) { DrawFrame((FrameDerived)frame.FrameSibling);

} if (frame.FrameFirstChild != null) { DrawFrame((FrameDerived)frame.FrameFirstChild);

} } Глава 13. Рендеринг скелетной анимации Здесь выполняется знакомый нам обход иерархии, программа пытает­ ся нарисовать каждый контейнер MeshContainer, на который ссылается используемый фрейм. Для рендеринга соответствующего MeshContainer добавьте к приложению метод, приведенный в листинге 13.9.

Листинг 13.9. Рендеринг контейнера MeshContainer. private void DrawMeshContainer(MeshContainerDerived mesh, FrameDerived frame) { // Is there skin information? if (mesh.Skinlnformation != null) { int attribldPrev = -1;

// Draw for (int iattrib = 0;

iattrib < mesh.NumberAttributes;

iattrib++) { int numBlend = 0;

BoneCombination[] bones = mesh.GetBones() ;

for (int i = 0;

i < mesh.Numberlnfluences;

i++) { if (bones[iattrib].BoneId[i] != -1) { numBlend = i;

} } if (device.DeviceCaps.MaxVertexBlendMatrices >= numBlend + 1) { // first calculate the world matrices for the current set of // blend weights and get the accurate count of the number of // blends Matrix[] offsetMatrices = mesh.GetOffsetMatrices();

FrameDerived[] frameMatrices = mesh.GetFrames();

for (int i = 0;

i < mesh.Numberlnfluences;

i++) { int matrixlndex = bones[iattrib].BoneId[i];

if (matrixlndex != -1) { Matrix tempMatrix = offsetMatrices[matrixlndex] * frameMatrices[matrixlndex]. CombinedTransformationMatrix;

device.Transform.SetWorldMatrixByIndex(i, tempMatrix);

} } Часть III. Более совершенные методы построения графики device.RenderState.VertexBlend = (VertexBlend)numBlend;

// lookup the material used for this subset of faces if ((attribldPrev != bones[iattrib].Attribld) || (attribldPrev == -1)) { device.Material = mesh.GetMaterials() [ bones[iattrib].Attribld].Material3D;

device.SetTexture(0, mesh.GetTextures() [ bones[iattrib].Attribld]) ;

attribldPrev = bones[iattrib].Attribld;

} mesh.MeshData.Mesh.DrawSubset(iattrib);

} } ) else // standard mesh, just draw it after setting material properties { device.Transform.World = frame.CombinedTransformationMatrix;

ExtendedMaterial[] mtrl = mesh.GetMaterials();

for (int iMaterial = 0;

iMaterial < mtrl.Length;

iMaterial++) { device.Material = mtrl[iMaterial].Material3D;

device.SetTexture(0, mesh.GetTextures() [iMaterial]);

mesh.MeshData.Mesh.DrawSubset(iMaterial);

} } } Размер листинга данного метода впечатляет, хотя, если рассматривать его по частям, это выглядит не так сложно. Вначале проверяются данные об оболочке, значение Skinlnformation. Если контейнер Meshcontainer не содержит никакой скелетной информации, объект будет рендирован точ­ но так же, как мы делали это ранее при отображении обычных объектов. При наличии скелетной информации способы рендеринга могут быть различными. АНИМАЦИЯ ОБЪЕКТОВ, НЕ ИМЕЮЩИХ КАРКАСА Если объект не содержит никакой скелетной информации, это не означает, что объект не может быть анимирован. Если в качестве анимации, включенной в объект, используется стандартная матрич­ ная операция (например: масштабирование, перемещение или вра­ щение), нет никакой необходимости в каркасах и скелетных данных. Однако система анимации будет переписывать матрицы для ваше­ го объекта в соответствии с обычными методами.

Глава 13. Рендеринг скелетной анимации Далее в листинге для каждого принимаемого атрибута объекта (набор материалов, текстур и пр.) выполняются различные операции. Вначале просматривается таблица комбинаций каркасов, и определяются весо­ вые коэффициенты, которые объект будет использовать. Файл примера, включенный к CD диск, использует максимум четыре весовых парамет­ ра, при этом подразумевается, что устройство имеет возможность объе­ динять и оперировать большим количеством матриц. После проверки того, что устройство может отображать объект с оп­ ределенными весовыми коэффициентами, необходимо выполнить пре­ образование координат. Для каждого элемента, найденного по идентификационному номеру в таблице комбинаций каркасов, матрица смешения объединяется с об­ шей матрицей преобразования фрейма, и текущая индексированная ми­ ровая матрица, преобразованная в результирующую матрицу, сохраня­ ется. Это позволит приложению Direct3D отобразить каждую интерпо­ лированную вершину (blended vertex) в соответствии с преобразовани­ ем координат. После выполнения этих операций устанавливается состояние рейде­ ра интерполированной вершины в соответствии с номером numBlend объекта. И, наконец, устанавливаются и затем отображаются материал и текстура данного подмножества. Запуская приложение, мы должны увидеть на экране модель, идущую по направлению к нам, рис.13. Рис. 13.1. Аиимированный объект Часть III. Более совершенные методы построения графики ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ «INDEXED MESH» ДЛЯ АНИМАЦИИ Ранее мы обсуждали использование для рендеринга вершин индек­ сных буферов, что позволяло сократить число необходимых для ри­ сования полигонов и более рационально использовать память при отображении. При визуализации сложных объектов, таких как в на­ шем примере, преимущества использования индексированных объектов налицо. Кроме того, это позволяет упростить написание кода. Прежде чем модифицировать наш объект, необходимо выполнить некоторые действия. Сначала следует добавить новый объект к про­ изводному классу mesh container:

private int numPal = 0;

public int NumberPaletteEntries { get ( return numPal;

} set ( numPal = value;

} } Это позволит сохранить число каркасных матриц, которые могут ис­ пользоваться для матричной палитры skinning при преобразовании нашего объекта. Затем, поскольку мы больше не будем использо­ вать стандартную интерполяцию вершин для нашей анимации, не­ обходимо переписать метод инициализации, проверяя возможность поддержки устройством новых операций. Для этого перепишите строку, включающую данную проверку:

if (hardware.MaxVertexBlendMatrixIndex >= 12) Все, что теперь требуется, — заменить запрос создания объекта (листинг 13.10) и затем вызов рисунка (листинг 13.11).

Листинг 13.10. Создание объекта Mesh. public void GenerateSkinnedMesh(MeshContainerDerived mesh) { if (mesh.Skinlnformation == null) throw new ArgumentException();

int numMaxFacelnfl;

MeshFlags flags = MeshFlags.OptimizeVertexCache;

MeshData m = mesh.MeshData;

using(IndexBuffer ib = m.Mesh.IndexBuffer) { numMaxFacelnfl = mesh.SkinInformation.GetMaxFaceInfluences(ib, m.Mesh.NumberFaces);

Глава 13. Рендеринг скелетной анимации } / / 1 2 entry palette guarantees that any triangle (4 independent // influences per vertex of a tri) can be handled numMaxFacelnfl = (int)Math.Min(numMaxFacelnfl, 12);

if (device.DeviceCaps.MaxVertexBlendMatrixIndex + 1 >= numMaxFacelnfl) { mesh.NumberPaletteEntries = (int)Math.Minf(device.DeviceCaps. MaxVertexBlendMatrixIndext 1) / 2, mesh.Skinlnformation.NumberBones);

flags != MeshFlags.Managed;

} BoneCombination[] bones;

int numlnfl;

m.Mesh = mesh.Skinlnformation.ConvertToIndexedBlendedMesh(m.Mesh, flags, mesh.GetAdjacencyStreamf), mesh.NumberPaletteEntries, out numlnfl, out bones);

mesh.SetBones(bones);

mesh.Numberlnfluences = numlnfl;

mesh.NumberAttributes = bones.Length;

mesh.MeshData = m;

Вначале определяется максимальное число влияний поверхности (face influences), в этом объекте их необходимо по крайней мере 12 (4 значения интерполяции для каждой вершины в треугольнике). Полагая, что наше устройство поддерживает эту возможность (про­ веряется в методе инициализации), мы вычисляем число входов палитры (либо число каркасов, либо половина от максимального числа поддерживаемых влияний поверхности). Теперь мы можем преобразовывать наш объект в индексный и со­ хранять те же самые данные, которые мы использовали в нашей неиндексированной версии. Итак, заменяем вызов рисунка, см. лис­ тинг 13.11. Для краткости мы включаем только код внутри блока, под­ разумевая, что значение оболочки «skin» ненулевое.

Листинг. 13.11. Вызов рисунка. if (mesh.Numberlnfluences == 1) device.RenderState.VertexBlend = VertexBlend.ZeroWeights;

else device.RenderState.VertexBlend = (VertexBlend)(mesh.Numberlnfluences - 1);

if (mesh.Numberlnfluences > 0) device.RenderState.IndexedVertexBlendEnable = true;

BoneCombination[] bones = mesh.GetBonesf);

for(int iAttrib = 0;

iAttrib < mesh.NumberAttributes;

iAttrib++) { Часть III. Более совершенные методы построения графики // first, get world matrices for (int iPaletteEntry = 0;

iPaletteEntry < mesh.NumberPaletteEntries;

++iPaletteEntry) { int iMatrixIndex = bones[iAttrib].BoneId[iPaletteEntry];

if (iMatrixIndex != -1) { device.Transform.SetWorldMatrixByIndex(iPaletteEntry, mesh.GetOffsetMatricesO [iMatrixIndex] * mesh.GetFramesO [iMatrixIndex]. CombinedTransformationMatrix);

} } // Setup the material device.Material = mesh.GetMaterials()[bones[iAttrib].Attribld].Material3D;

' device.SetTexture(0, mesh.GetTextures()[bones[iAttrib].Attribld]) ;

// Finally draw the subset mesh.MeshData.Mesh.DrawSubset(iAttrib);

} Этот метод более прост в написании. Вначале мы определяем состоя­ ние рендера интерполированной вершины как «-1». Затем, если есть влияния (для случая скелетной анимации), состояние рендера прини­ мает значение «true» для параметра IndexedVertexBlendEnable. Остальная часть кода подобна предыдущему методу. Для каждого входа палитры записывается соответствующая мировая матрица с учетом индекса общей матрицы смещения и фреймов общей мат­ рицы преобразования. После записи мировых матриц можно уста­ навливать материалы, текстуры и рисовать каждое подмножество. Использование индексированного mesh-объекта более предпочти­ тельно в плане быстродействия (от 30% и более, в зависимости от типа данных), чем использование обычного mesh-объекта.

Краткие выводы В этой главе мы ознакомились с основами анимации, включая скелет­ ную анимацию. Основные рассмотренные в этой главе темы. • Создание и загрузка иерархической системы фреймов. • Просмотр иерархии, рендеринг фреймов. Формирование скелетных данных. Использование контроллера анимации. Использование «indexed mesh». В следующей главе мы обсудим использование звука в создаваемых приложениях.

ЧАСТЬ IV ЗВУК И УСТРОЙСТВА ВВОДА Глава 14. Добавление звука Глава 15. Управление устройствами ввода Часть IV. Звук и устройства ввода Глава 1 4. Добавление звука Ни одно графическое приложение или игра не будет живым, если вы не добавили соответствующее окружение, атмосферу и особенно звук. Можете ли вы представить известную игру Рас-Man без знакомого звуко­ вого сопровождения? В этой главе мы изучим добавление звука в наши приложения. Темами, которые мы обсудим, являются. • Загрузка и проигрывание статических звуков. • Проигрывание звуков в 3D пространстве. • Проигрывание звуков с эффектами.

Включение пространства имен SOUND Звуковые ресурсы, которые мы будем использовать, отсутствуют в списке имен Direct3D, вместо этого необходимо использовать простран­ ство имен Microsoft.DirectX.DirectSound. Для каждого из примеров, ко­ торые будут рассмотрены в этой главе, необходимо добавить ссылку на указанную сборку, также как и директиву using.

Загрузка и проигрывание статических звуков В начале выясним, что мы хотим сделать для добавления звука? Напри­ мер, иметь некоторый тип звуковых данных, выводимых на динамики, при­ ложенные к системе. Подобно классу Device для приложения Direct3D, который управляет аппаратным обеспечением компьютерной графики, в приложении DirectSound имеется класс Device, который управляет звуко­ выми аппаратными средствами. Поскольку оба этих классов совместно используют одно и тоже название (в различных пространствах имен), не­ обходимо строго описывать ссылки на переменную Device, если мы под­ разумеваем использование Direct3D и DirectSound в одном файле кода. Подобно графическим буферам в Direct3D, приложение DirectSound имеет буферы для обработки, передачи и вывода звуковых данных. В DirectSound существуют только два типа буферов: либо основной класс Buffer, либо производный от него объект SecondaryBuffer. Итак, вначале мы создаем новый проект, включаем в него ссылки на DirectSound, а затем добавляем следующие объявления переменных: private Device device = null;

private SecondaryBuffer sound = null;

Для проигрывания загружаемых звуковых данных нам нужно иметь одно звуковое устройство и один буфер SecondaryBuffer. Теперь мы можем запи­ сать метод инициализации звука InitializeSound, который позволяет запус Глава 14. Добавление звука кать и проигрывать звук. Поскольку указанный метод будет ссьшаться на звуковой файл, необходимо это предусмотреть. Используемый в этом мето­ де звуковой файл скопирован из DirectX SDK. Добавьте следующий код:

public void InitializeSound() { device = new Device ();

device.SetCooperativeLevel(this, CooperativeLevel.Normal);

sound = new SecondaryBuffer(@"..\..\drumpad-crash.wav", device);

sound.Play(0, BufferPlayFlags.Default);

} Приведенный здесь код достаточно прост. Используя простой конструк­ тор параметров, мы создаем новое устройство (другие варианты загрузки этого объекта мы рассмотрим позднее) и определяем уровень совместного доступа к устройству CooperativeLevel.Normal. Поясним подробнее. Звуковые устройства компьютера используются многими приложени­ ям. Система Windows может подавать звуковой сигнал каждый раз, когда возникает ошибка. Система Messenger может подавать звуковой сигнал, когда кто-то вошел или зарегистрировался, и все это может происходить, например, во время прослушивания музыки. Уровень совместного досту­ па используется, чтобы определить степень совместного с другими прило­ жениями использования звуковых карт. В нашем примере данный пара­ метр определен по умолчанию. Это подразумевает многозадачный режим и совместно используемые ресурсы. Описание других уровней доступа, например, приоритетных, можно найти в документации DirectX SDK. После установки уровня доступа можно создать или загрузить буфер из файла (код, включенный в CD диск DirectX SDK, использует простой звук) и затем уже проигрывать звук. Первый параметр вызова проигры­ вания звука — приоритет, который имеет смысл только, если буфер со­ здается с задержкой (чего мы пока не имеем). Теперь, используя задан­ ные по умолчанию флажки, вызываем процедуру проигрывания содер­ жимого буфера:

static void Main() { using (Forml frm = new Forml()) { frm.InitializeSoundO ;

Application.Run(frm);

} } Все достаточно просто. Несколько строк программы, и при запуске приложения появляется звук.

Часть IV. Звук и устройства ввода Конструктор для буфера SecondaryBuffer требует несколько отличных параметров. Рассмотрим две основных разновидности этого конструктора: public SecondaryBuffer ( System.String fileName, Microsoft.DirectX.DirectSound.BufferDescription desc, Microsoft.DirectX.DirectSound.Device parent ) public SecondaryBuffer ( System.10.Stream source, System.Int32 length, Microsoft.DirectX.DirectSound.BufferDescription desc, Microsoft.DirectX.DirectSound.Device parent ) Все конструкторы вторичного буфера оперируют с устройством, ко­ торое будет использоваться для проигрывания звуковых данных. Конст­ рукторы могут принимать либо имя JileName файла загружаемых звуковых данных, либо поток, который содержит звуковые данные. Для опи­ сания различных опций буфера, который вы создаете, существует конст­ руктор BufferDescription. Все создаваемые буферы имеют характеристи­ ки, и, если вы не задаете конструктор BufferDescription, то значения его параметров и флажков устанавливаются по умолчанию. Свойства конст­ руктора BufferDescription приведены в таблице 14.1. Таблица 14.1. Свойства и флажки конструктора BufferDescription Название BufferBytes Описание Размер буфера в байтах (чтение-запись). Если вы создаете первичный буфер или буфер из потока или звукового файла, вы можете оставить этотму параметру нулевое значение Атрибут чтение-запись. Является булевой переменной, уточняет, нужна ли точная позиция проигрывания Атрибут чтение-запись. Является булевой переменной, сообщает, можно ли манипулировать вашим буфером в 3Dпространстве. Эта опция не может использоваться, если ваш буфер содержит формат стерео (два канала), или опция ControlPan установлена в значение «true» Атрибут чтение-запись. Является булевой переменной, указывает на то, что буфер может или не может использовать обработку эффектов. Чтобы использовать эти эффекты, необходимо иметь 8- или 16-разрядные аудио данные формата РСМ, при этом возможно использование не более двух каналов (стерео) CanGetCurrentPosition Contro3D ControlEffects Глава 14. Добавление звука Название ControlFrequency Описание Атрибут чтение-запись. Является булевой переменной, устанавливает возможность изменения частоты обращения к буферу (при значении «true») ControlPan Атрибут чтение-запись. Является булевой переменной, определяет возможность поддержки опции panning (панорама), не может использоваться с флажком Contro3D Атрибут чтение-запись. Является булевой переменной, показывает, поддерживает ли буфер указание на позицию Атрибут чтение-запись. Является булевой переменной, указывает на то, что буфер может или не может управлять громкостью Атрибут чтение-запись. Является булевой пере­ менной, указывает, может ли буфер присоединять­ ся к устройству во время проигрывания. Данный флажок должен принимать значение «true» для буферов, поддерживающих голосовое управление. Поразрядная комбинация перечисления BufferDescriptionFlags. Значения для этих элементов установлены как логические переменные, например: desc. ControlPan = true;

desc.ControlEffects = true;

что аналогично следующему: desc.Flags = BufferDescriptionFlags.ControlPan BufferDescriptionFlags.ControlEffects;

ControlPositionNotify ControlVolume DeferLocation Flags Format Атрибут чтение-запись. Определяет акустический формат создаваемых или загружаемых в буфер звуковых данных Атрибут чтение-запись. Булева переменная. При значении по умолчанию («false») звук проигрывается, если приложение активно. При значении «true» звук будет проигрываться в любом состоянии приложения Атрибут чтение-запись. Идентификатор GUID. Определяет алгоритм, используемый для 3Dвиртуализации. Вы можете использовать любую из констант DSoundHelper.Guid3Dxxxx, перечисленных в Управляемом DirectX для встроенных режимов GlobalFocus Guid3DAlgorithm 280 Название LocatelnHardware Описание Часть IV. Звук и устройства ввода Атрибут чтение-запись, булева переменная. Определяет обработку буфера аппаратными средствами. Если значением является «true», a необходимая аппаратная поддержка отсутствует, при создании буфера произойдет ошибка Атрибут чтение-запись, булева переменная. Определяет обработку буфера программными средствами, значение «true» разрешает программную обработку независимо от аппаратных ресурсов Атрибут чтение-запись, булева переменная, определяет остановку проигрывания при достижении максимальной дистанции. Параметр применим только к программным буферам Атрибут чтение-запись. Показывает, является ли буфер первичным. Атрибут чтение-запись, булева переменная. Определяет размещение буфера в аппаратной памяти (значение «true»), если она поддерживается, либо в программной памяти, если аппаратная недоступна. При использовании данного флажка вы не можете задавать значение «true» для флажка ControlEffects (см. выше) Атрибут чтение-запись. Булева переменная. Определяет использование закрепленного фокуса. Как уже упоминалось при обсуждении флажка глобального фокуса, значение по умолчанию для DirectSound прекращает проигрывание буфера, если ваше окно не активно. Если флажок установлен в значение «true», буфер будет продолжать проигрывание, даже если окно уже не использует DirectSound LocatelnSoftware Mute3DAtMaximumDistance PrimaryBuffer StaticBuffer StickyFocus Таким образом, имеется несколько опций для буфера. В следующем примере мы рассмотрим работу некоторых из них. Мы постараемся уп­ равлять некоторыми параметрами, как-то: перетекание звука, панорама звука и т. д. Чтобы выполнить это, нам необходимо использовать определение клас­ са «buffer description». Добавьте следующий код после вызова установки уровня доступа SetCooperativeLevel: BufferDescription desc = new BufferDescription( desc.ControlPan = true;

desc.GlobalFocus = true;

Глава 14. Добавление звука По существу, мы сообщаем приложению DirectSound, что создадим буфер и будем управлять панорамой звука. Также определяем буфер как глобальную переменную. Теперь мы должны определить процедуру создания буфера с помо­ щью конструктора, использующего описание уже созданного нами буфе­ ра. Для улучшения эффекта мы зацикливаем буфер на время проигрыва­ ния. Для выполнения этого измените код следующим образом:

sound = new SecondaryBuffer(@".,\..\drumpad-crash.wav",desc, device);

sound.Play(0, BufferPlayFlags.Looping);

При запуске приложения произойдет непрерывное проигрывание зву­ кового файла, но при этом мы пока еще не изменяли панорамное значение нашего буфера. Для разнообразия можно обновлять его каждые 50 милли­ секунд. Для в этого в окне design view нашей формы добавим таймер и установим интервал 50 миллисекунд. Добавьте следующий код обработ­ чика таймера (либо дважды кликните на таймер в окне design view):

private void timerl_Tick(object sender, System.EventArgs e) { // Adjust the pan sound.Pan *= -1;

} Панорамное значение — целочисленное значение, которое определя­ ет позицию стерео звука;

чем «отрицательнее» значение, тем больше звука выходит из левого динамика. Чем больше положительное значение — из правого. Нулевое значение (значение по умолчанию) уравнивает звук из обоих динамиков. Теперь установите начальное значение панорамирова­ ния на левый динамик и запустите таймер. Добавьте этот код непосред­ ственно после вызова проигрывания в InitializeSound:

sound.Pan = -5000;

timerl.Enabled = true;

Добавив таймер, запускаем приложение. Мы должны услышать про­ игрывание нашего звукового файла, при этом звук должен перетекать из динамика в динамик. Как оказалось, использовать звуки для нашего при­ ложения — не такая сложная процедура.

Использование 3D-звука Многие компьютерные мультимедийные системы в настоящее время используют новейшие модели колонок и звуковых плат класса «Hi-End».

Часть IV. Звук и устройства ввода Уже трудно кого-нибудь удивить системами объемного звучания, а для разработчиков компьютерных игр это вообще является нормой. Приложение DirectSound API уже имеет возможность запускать звуки в трехмерном пространстве и может использовать их особенности (если компьютер поддерживает эти опции). Опции управляются посредством объектов Buffer3D (чтобы управлять источником звука) и Listener3D (что­ бы управлять позицией и ориентацией слушателя). Обратите внимание на то, что каждый из этих конструкторов использует ЗD-звук. Эти буфе­ ры должны создаваться с разрешенным флажком ControBD (значение «true»). Какие же преимущества дает ЗD-звук? Это наиболее ощутимо, когда мы находимся в кинотеатре, где используются системы объемного звуча­ ния, когда различные объекты издают звук из различных динамиков. Со­ временные игры также требуют все большего развития систем воспроиз­ ведения звука. Когда мы хотим манипулировать ЗD-звуком, мы можем управлять ис­ точниками звука или «ушами» (то есть слушателем), принимающими звук. Для начала, используя объект Buffer3D, мы можем рассмотреть переме­ щение вокруг источника звука Этот объект-класс предназначен для управления ЗD-настройками бу­ фера. Параметры объекта Buffer3D и их описание приведены в табли­ це 14.2. Таблица 14.2. Параметры и свойства объекта Buffer3D Название ConeAngles Описание Атрибут чтение-запись. Используется для установки углов относительно конуса проекции. Оба угла (внутренний и внешний) определены в градусах. Звуки в пределах внутреннего конического угла находятся на нормальном уровне, в то время, как вне его — на окружающем фоновом уровне Атрибут чтение-запись. Используется для ориентирова­ ния конуса проекции. Приложение DirectSound автома­ тически нормализует соответствующий вектор (цент­ ральный вектор конуса проекции) Атрибут чтение-запись. Используется для регулировки уровня звука, находящегося за пределами угла звукового конуса Атрибут чтение-запись. Булева переменная. Определяет, изменяются ли регулируемые параметры сразу либо они задерживаются, пока пользователь изменяет установки. По умолчанию данное значение «false», то есть свойства изменяются сразу ConeOrientation ConeOutsideVolume Deferred Глава 14. Добавление звука Название MaxDistance Описание Атрибут чтение-запись. Определяет максимальное расстояние до слушателя, при котором звук еще не затухает MinDistance Атрибут чтение-запись. Определяет минимальное расстояние до слушателя, при котором звук начинает затухать Атрибут чтение-запись. Определяет режим звуковой обработки. Значение по умолчанию — режим Mode3D.Normal. Вы можете также использовать Mode3D.Disable, который отключает обработку 3Dзвука, или Mode3D.HeadRelative, который подразумевает регулировку звука слушателем Атрибут чтение-запись. Определяет текущее местоположение источника звука в мировых координатах Атрибут чтение-запись. Определяет скорость перемещения источника звука (по умолчанию значение в метрах на секунду) Mode Position Velocity Имеется и другой флажок AllParameters, позволяющий изменять все установки сразу. Можно попробовать переписать последний пример, ис­ пользующий параметр панорамы, установив вместо него параметр 3Dprocessing. Звуковой файл, который мы использовали до настоящего времени, имеет стерео звучание, т. е. имеет два отдельных канала для левого и правого динамиков. Чтобы использовать трехмерную обработку, мы дол­ жны использовать файл с монозвуком (или стерео, но с только одним каналом). Таким образом, для нашего примера будем загружать монозву. ковой файл с CD диска DirectX SDK. Добавьте ссылку на объект Buffer3D, который будет использоваться при управлении трехмерным буфером: private Buffer3D buffer = null;

Также необходимо переписать метод инициализации InitializeSound, чтобы использовать ЗD-обработку вместо панорамирования, которое мы использовали прежде: public void InitializeSound!) ( device = n w Device();

e device.SetCooperativeLevel(this, CooperativeLevel.Normal);

BufferDescription desc = n w BufferDescription();

e Часть IV. Звук и устройства ввода desc.Control3D = true;

desc.GlobalFocus = true;

sound = new SecondaryBuffer(@".A..\drumpad-bass_drum.wav", desc, device);

buffer = new Buffer3D(sound);

sound.Play(0, BufferPlayFlags.Looping);

buffer.Position = new Vector3(-0.1f, O.Of, O.Of);

timerl.Enabled = true;

} Мы заменили панорамирование трехмерной обработкой, плюс, заме­ нили звуковой файл. Затем создали объект Buffer3D, установив его на непрерывное проигрывание (значение loop). Далее устанавливается про­ игрывание по левому каналу (напомним, что по умолчанию это значение О, 0, 0). Теперь необходимо переписать код таймера и определить круговое звучание:

private void timerl_Tick(object sender, System.EventArgs e) { // Adjust the position buffer.Position *= mover;

if ((Math.Abs(buffer.Position.X) > MoverMax) && (mover == MoverUp)) { mover = MoverDown;

} if ((Math.Abs(buffer.Position.X) < MoverMin) && (mover == MoverDown)) { mover = MoverUp;

} ) Как мы видим, в этом коде появились несколько новых переменных и констант, определяющих круговое звучание. Для правильной компиляции их необходимо добавить к нашему классу:

private private private private private const const const const float float float float float mover MoverMax = 35.Of;

MoverMin = 0.5f;

MoverUp = -1.05f;

MoverDown = -0.95f;

= MoverUp;

Запуская приложение, обратите внимание на то, как звук перемещает­ ся от динамика к динамику, а также медленно удаляется, пропадает и возвращается вновь. Приложение будет выполняться, пока вы не выйде­ те из него.

Глава 14. Добавление звука Управление слушателем Объект Listener3D создается и используется так же, как объект Buffer3D. Однако, вместо управления фактическим звуковым источни­ ком (которых может быть несколько), мы имеем дело непосредственно со слушателем, который может быть в устройстве только одним. По этой причине, при создании объекта listener мы не можем использовать SecondaryBuffer, а только первичный объект Buffer. Прежде чем написать код, используя объект listener, необходимо учесть свойства и параметры настройки, которые мы можем изменять (анало­ гично тому, что мы делали с объектом 3D Buffer). Параметры и свойства приведены в таблице 14.3. Таблица 14.3. Параметры и свойства объекта Listener3D Название CommitDeferredSettings Описание При установленной задержке параметров настройки в 3D буферах или листенере этот метод перешлет все новые значения в то же самое время. Метод не выполняется при отсутствии задержки установленных изменений Атрибут чтение-запись. Устанавливает число метров в векторном модуле Атрибут чтение-запись. Определяет коэффициент для эффекта Доплера. Любой буфер, который работает со скоростью перемещения источника звука, учитывает изменения тона, связанные с эффектом Доплера Определяет то, изменяются ли регулируемые параметры сразу либо насколько они задерживаются, пока пользователь изменяет установки. По умолчанию данное значение «false», то есть свойства изменяются сразу Атрибут чтение-запись. Определяет местоположе­ ние слушателя. Приложение DirectSound автомати­ чески нормализует соответствующий вектор Атрибут чтение-запись. Определяет параметр rolloff factor — скорость затухания по мере увеличения расстояния Атрибут чтение-запись. Определяет текущее местоположение слушателя в мировых координатах Атрибут чтение-запись. Определяет скорость перемещения слушателя (по умолчанию значение в метрах на секунду) DistanceFactor DopplerFactor Deferred Orientation RolloffFactor Position Velocity Часть IV. Звук и устройства ввода Теперь мы должны взять имеющийся пример, в котором перемещает­ ся звуковой буфер, и переписать код, вставив в него перемещение слуша­ теля. Можно убрать ссылку на объект 3D buffer, который мы использова­ ли, и заменить соответствующую переменную нижеприведенными:

private Listener3D listener = null;

private Microsoft.DirectX.DirectSound.Buffer primary = null;

Мы должны использовать первичный буфер для создания объекта listener. Также необходимо учесть, что поскольку имя класса Buffer нахо­ дится в пространстве имен системы, мы должны полностью определить переменную buffer. Для этого нужно переписать метод инициализации InitializeSound. Замените код создания звукового буфера на следующий:

BufferDescription primaryBufferDesc = new BufferDescriptionO;

primaryBufferDesc.Control3D = true;

primaryBufferDesc.PrimaryBuffer = true;

primary = new Microsoft.DirectX.DirectSound.Buffer(primaryBufferDesc, device);

listener = new Listener3D(primary);

listener.Position = new Vector3(0.1f, O.Of, O.Of);

Здесь мы создаем первичный буфер, объявляем объект listener и затем устанавливаем его местоположение (несколько «левее» от заданного по умолчанию). Теперь мы должны переписать код таймера, чтобы переме­ щать слушателя:

private void timerl_Tick(object sender, System.EventArgs e) { // Adjust the position listener.Position *= mover;

if ((Math.Abs(listener.Position.X) > MoverMax) && (mover == MoverUp)) mover = MoverDown;

if ((Math.Abs(listener.Position.X) < MoverMin) && (mover == MoverDown)) mover = MoverUp;

} Легко заметить, что мы не сделали ничего, кроме замены буфера на слушателя. Этот пример должен проигрываться аналогично предыду­ щему.

Глава 14. Добавление звука ФЛАЖОК CONTROL3D Важно обратить внимание, что для создания объекта listener исполь­ зуется флажок Control3D, в противном случае звук будет проигры­ ваться с одинаковым уровнем громкости для всех динамиков.

Использование звуковых эффектов Кроме обычных ЗD-манипуляций со звуком, мы в нашем приложении можем использовать различные эффекты. Например, звук шагов по дере­ вянным полам в маленькой комнате отличался бы от звука шагов, разда­ ющихся в большом концертном зале. Крики в пещере вызвали бы много­ кратное эхо, отражаемое от стен пещеры. Эти эффекты — неотъемлемая часть того реального погружения, которое вы будете пробовать создавать при разработке игры. БУФЕРЫ, ПОДДЕРЖИВАЮЩИЕ ЗВУКОВЫЕ ЭФФЕКТЫ Звуковые эффекты и методы их программирования могут быть реа­ лизованы только с использованием вторичного буфера SecondaryBuffer и соответствующих ссылок на этот объект. К счастью, действие звуковых эффектов в достаточной мере описано и классифицировано, и несколько таких эффектов уже встроены в DirectSound API. Перепишем наш старый пример, используя некоторые звуковые эф­ фекты. Во-первых, удалите таймер, так как здесь он не будет использоваться. Затем перепишите метод инициализации InitializeSound следующим об­ разом:

public void InitializeSound() { device = new Device));

device.SetCooperativeLevel(this, CooperativeLevel.Normal);

BufferDescription desc = new BufferDescription() ;

desc.ControlEffects = true;

desc.GlobalFocus = true;

sound = new SecondaryBuffer(@"..\..\drumpad-crash.wav",desc, device);

EffectDescription[] effects = new EffectDescription[l];

effects[0].GuidEffectClass = DSoundHelper.StandardEchoGuid;

sound.SetEffects(effects);

sound.Play(0, BufferPlayFlags.Looping) ;

} Часть IV. Звук и устройства ввода Самое большое изменение здесь — замена управления панорамой зву­ ка на управление звуковыми эффектами. Создается также массив описа­ ний эффекта (в настоящем примере с только одним членом), и задается стандартный эффект — эхо. Затем созданные эффекты пересылаются, и запускается проигрывание буфера. УСТАНОВКА ЭФФЕКТОВ Эффекты могут быть установлены только на остановленном буфе­ ре. Если буфер проигрывается, вызов SetEffects игнорируется. Вы можете просмотреть свойство состояния буфера (Status property, статус буфера), чтобы определить, проигрывается буфер или нет. Также вы можете вызывать оператор Stop перед обращением к эф­ фектам. Определив и установив эффекты, мы можем запустить приложение. При проигрывании мы должны услышать эффект эха, которого не было до сих пор. Возможно и добавление «пачки» наложенных друг на друга эффектов (stacks). Чтобы использовать эффект эха вместе с эффектом гребня, сопровождаемым эффектом искажения, мы можем переписать метод следующим образом: EffectDescription[] effects = effects[0].GuidEffectClass = effects[l].GuidEffectClass = effects[2].GuidEffectClass = sound.SetEffects(effects);

new EffectDescription[3] ;

DSoundHelper.StandardEchoGuid;

DSoundHelper.StandardFlangerGuid;

DSoundHelper.StandardDistortionGuid;

Мы получили некоторую смесь звуков барабана кимвал и работающе­ го двигателя самолета (этот звук очень понравился автору книги — прим. ред.). Как уже упоминалось, существует несколько встроенных типов эффектов, см. таблицу 14.4. Таблица 14.4. Типы встроенных звуковых эффектов Название Chorus Описание Эффект хора, удваивает голоса, выводя первоначальный звук второй раз с небольшой задержкой и слегка модулируя эту задержку Эффект сжатия — по существу уменьшает сигнал до некоторой амплитуды.

Compression Глава 14. Добавление звука Название Distortion Описание Эффект искажения. Осуществляется добавлением гармоник к сигналу, при увеличении уровня изменяется форма волны Эффект эхо, повторяет многократно первоначальный звук с заданной задержкой и спадом амплитуды последующего сигнала Echo Environment Reverberation. Эффект отражения или раскаты. Осуществляется спецификацией 3D Audio Level 2 (I3DL2) Flange Эффект гребня, напоминает эффект хора, плюс эффект эхо только с маленькой задержкой и сменой тона через какое то время Просто модулирует амплитуду сигнала Этот эффект действует как эквалайзер, позволяя усиливать или подавлять сигналы определенной частоты Этот эффект предназначен для использования вместе с музыкой, напоминает эффект раскатов Gargle Parametric Equalizer Waves Reverberation Теперь мы можем выбрать и применить любой из эффектов, выпол­ нив предварительно соответствующие настройки. Вы можете делать это для вторичного буфера с помощью метода GetEffects. Чтобы лучше слы­ шать изменения при установке, необходимо отключить остальные эф­ фекты, за исключением эха. Добавьте следующую секцию кода сразу после вызова SetEffects: EchoEffect echo = (EchoEffect)sound.GetEffects(0);

EffectsEcho param = echo.AllParameters;

param.Feedback = 1.Of;

param.LeftDelay = 1060.2f;

param.RightDelay = 1595.3f;

param.PanDelay = 1;

echo.AllParameters = param;

Здесь мы используем метод GetEffects для создания объекта EchoEffect, определяющего все изменяемые параметры используемого эффекта эха. Теперь проиграв звук с изменениями, выполненными в дан­ ном методе, и без них, мы должны сразу почувствовать разницу. Таким образом, мы можем управлять любым загружаемым эффектом.

10 Зак. 290 ИЗМЕНЕНИЕ СВОЙСТВ ЭФФЕКТА Часть IV. Звук и устройства ввода Даже при том, что вызов SetEffects может осуществляться только когда буфер остановлен, вы можете изменять параметры загружен­ ных эффектов в реальном времени, даже при проигрывающем бу­ фере.

Краткие выводы В этой главе были рассмотрены следующие вопросы. • Загрузка и проигрывание статических звуков. Проигрывание звуков в 3D пространстве. • Проигрывание звуков с эффектами. В нашей следующей главе мы ознакомимся с устройствами ввода, рас-' смотрим возможности управления клавиатурой, мышью и джойстиком.

Глава 15. Управление устройствами ввода Глава 15. Управление устройствами ввода К настоящему моменту мы охватили концепции построения 3D-rpaфики и рассмотрели вопросы использования звука. Тем не менее, до сих пор не обсуждались темы, связанные с пользова­ тельским интерфейсом. В главе 6, при написании простой игры, управ­ ляющей движением автомобиля, мы использовали только клавиатуру, не затрагивая мышь или джойстик. Также мы не рассматривали возможнос­ ти обратной связи при управлении приложением. В этой главе мы коснемся интерфейса Directlnput API и узнаем, как с ним работать, чтобы считывать и управлять данными, приходящими с устройств ввода. Разделы этой главы включают. Управление с клавиатуры. Управление с помощью мыши. Управление с помощью джойстика и игровой клавиатуры. • Работа с обратной связью.

Обнаружение устройств В первую очередь, чтобы использовать код, который будем обсуждать далее в этой главе, мы должны обеспечить необходимые ссылки на Directlnput. Нам также необходимо добавить ссылку на Microsoft.DirectX.Directlnput и директиву using для этого пространства имен. Даже если ваш компьютер является автономной системой, в нем име­ ется по крайней мере два устройства ввода данных: клавиатура и мышь. Дополняя машину различными USB-устройствами, ставшими доста­ точно распространенными в настоящее время, мы можем иметь несколь­ ко устройств ввода данных, которые мы должны уметь обнаруживать и.распознавать. Если вспомнить, в начале книги мы говорили о классе Manager, который входит в Direct3D. Приложение Directlnput имеет по­ добный класс, который мы будем использовать для этих задач. Самая простая вещь, которую мы можем сделать, состоит в том, что­ бы обнаружить все имеющиеся в системе устройства. Для этого мы мо­ жем использовать свойство devices property в соответствующем классе manager. Вначале необходимо создать и заполнить соответствующим об­ разом разветвленную схему для нашей формы. Для этой схемы в приложении необходимо добавить некоторые кон­ станты ключевых имен:

private const string AllItemsNode = "All Items";

private const string KeyboardsNode = "All Keyboard Items";

private const string MiceNode = "All Mice Items";

Часть IV. Звук и устройства ввода private const string GamePadNode = "All Joysticks and Gamepad Items";

private const string FeedbackNode = "All ForceFeedback Items";

Опираясь на эти константы, необходимо создать пять различных уз­ лов или папок в дереве разветвленной схемы: одна папка для всех уст­ ройств вашей системы, затем по одной секции для мыши, клавиатур, джойстиков и элементов обратной связи. Элементы, находящиеся в пап­ ке «All Items», должны быть продублированы в остальных папках. Вначале необходимо заполнить папку «All Items». Для этого мы со­ здадим функцию LoadDevices, целью которой является заполнение раз­ ветвленной схемы элементами, находящимися в системе. Добавьте ме­ тод, приведенный в листинге 15.1.

Листинг 15.1. Добавление устройств к разветвленной схеме. public void LoadDevices() { TreeNode allNodes = new TreeNode(AllItemsNode);

// First get all devices foreach(DeviceInstance di in Manager.Devices) { TreeNode newNode = new TreeNode (string. Format)" (01 - (1} ({2})\ di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid));

allNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(allNodes);

} Как вы можете видеть, класс DeviceList (производный от класса Devices) возвращает список структур Devicelnstance. Эта структура со­ держит всю полезную информацию относительно устройств, включая идентификатор GUID (использующийся при создании устройства), на­ звание продукта и тип устройства. Для проверки наличия устройства вы можете использовать и другие методы класса Manager. Вполне возможно иметь в системе и виртуаль­ ное устройство «available», которое, в принципе, поддерживается, но на данный момент отсутствует в системе. И, наконец, мы добавляем каждое найденное в системе устройство в соответствующую папку и добавляем папку в разветвленную схему. Теперь предположим, что мы хотели найти лишь некоторые типы ус­ тройств, например, только клавиатуру? Добавьте код листинга 15.2 в конце метода LoadDevices.

Глава 15. Управление устройствами ввода Листинг 15.2. Добавление клавиатуры к разветвленной схеме. // N w get all keyboards o TreeNode kbdNodes = new TreeNode(KeyboardsNode);

foreach(Device!nstance di in Manager.GetDevices(DeviceClass.Keyboard, EnumDevicesFlags.AttachedOnly)) { TreeNode newNode = new TreeNode(string.Format)"{0} - {1} ({2})", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid));

kbdNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(kbdNodes);

Здесь используется новый метод GetDevices класса Manager (вместо знакомого нам Devices), который позволяет определять типы перечис­ ленных устройств. В данном примере мы пытаемся обнаружить объекты keyboards, более того, мы хотим найти только те устройства, которые не­ посредственно подсоединены к системе. Для определения устройств дру­ гих типов код остается таким же, только указываются соответствующие значение используемого класса:

// Now get all mice TreeNode miceNodes = new TreeNode(MiceNode);

foreach(DeviceInstance di in Manager.GetDevices(DeviceClass.Pointer, EnumDevicesFlags.AttachedOnly)) { TreeNode newNode = new TreeNode(string.Format("{0} - {1} ({2})", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid)),• miceNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(miceNodes);

// Now get all joysticks and gamepads TreeNode gpdNodes = new TreeNode(GamePadNode);

foreach(DeviceInstance di in Manager.GetDevices(DeviceClass.GameControl, EnumDevicesFlags.AUDevices)) { TreeNode newNode = new TreeNode(string.Format("(0} - (1} ((21)", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid));

gpdNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(gpdNodes);

Часть IV. Звук и устройства ввода Обратите внимание, что указатель устройства мыши в классе имеет тип Pointer. Данный класс не подразумевает использование только для устройства мыши, это более общий тип. Экранные указатели Screen pointers, например, относятся к этой же категории. Теперь мы попробуем определить или найти в системе устройства с обратной связью. Оно не относится к какому-либо определенному типу устройств, это скорее характеристика или особенность, которую мы хо­ тим поддерживать. Эта проверка также весьма проста:

// N w get a l l Force Feedback items o TreeNode ffNodes = new TreeNode(FeedbackNode);

foreach(DeviceInstance di in Manager.GetDe?ices(DeviceClass.All, EnumDevicesFlags.ForceFeeback)) { TreeNode newNode = new TreeNode(string.Format("{0} - {1} ({2})", di.InstanceName, Manager.GetDeviceAttached(di.InstanceGuid) ? "Attached" : "Detached", di.InstanceGuid)) ;

ffNodes.Nodes.Add(newNode);

} treeViewl.Nodes.Add(ffNodes);

Полная структура кода остается такой же, мы только изменили метод перебора, чтобы перечислить только те устройства, которые поддержи­ вают обратную связь. ИСПОЛЬЗОВАНИЕ ПАНЕЛИ УПРАВЛЕНИЯ В классе Manager имеется метод RunControlPanel, который откры­ вает панель управления. Панель управления позволяет установить доступ к настройкам имеющихся в системе устройств.

Использование клавиатуры Каждое из устройств, использующихся в Directlnput, имеет свой соб­ ственный объект Device, подобно тому, как графическая или звуковая карта имеют свои объекты устройств. Количество устройств ввода в Directlnput может быть значительно больше, чем в графических или зву­ ковых картах. Создание устройства требует наличия идентификатора GUID. Это может быть просто компонент объекта SystemGuid, если мы хотим со­ здать клавиатуру или устройство мыши по умолчанию. Устройства, ко­ торые создаются при этом, достаточно универсальны и позволяют извле­ кать данные из любого устройства, поддерживаемого приложением Directlnput.

Глава 15. Управление устройствами ввода Первое устройство, с которого мы начнем, — клавиатура. Во-первых, нам понадобится переменная устройства:

private Device device = null;

Теперь у нас есть два пути, которые мы можем использовать в Directlnput. Мы можем применить обычный цикл и получить текущее состояние устройства для каждого фрейма, а можем установить Directlnput таким образом, чтобы изменить состояние игры в момент, когда появля­ ется информация об изменении устройства. Приведенный ниже код из­ начально использует первый механизм. Добавьте следующую функцию инициализации:

private bool running = true;

public void Initializelnput() { // Create our keyboard device device = new Device(SystemGuid.Keyboard);

device.SetCooperativeLevel(this, CooperativeLevelFlags.Background \ CooperativeLevelFlags.NonExclusive);

device.Acquire();

while(running) { UpdatelnputState();

Application.DoEvents();

} } Как вы видите, это — полный цикл ввода, вначале которого, исполь­ зуя стандартный модификатор GUID, создается устройство клавиатуры. Можно вспомнить, что в разделе DirectSound мы устанавливали уровень. совместного доступа для различных устройств системы. В нашем случае также могут использоваться различные флажки и их сочетания при объяв­ лении уровней совместного доступа, см. таблицу 15.1. Таблица 15.1. Уровни совместного доступа в Directlnput Флажок Background Описание Фоновый доступ. Устройство может использоваться на заднем плане или может быть вызвано в любое время, даже если связанное окно не является активным Активный доступ. Устройство может использоваться только при активном окне, в противном случае, не может быть вызвано Foreground 296 Флажок Exclusive Описание Часть IV. Звук и устройства ввода Эксклюзивный режим. Устройство имеет статус монопольного доступа из приложения. Кроме него, никакое другое приложение не может иметь такой же статус доступа, однако неэксклюзивные запросы возможны. Из соображений безопасности на некоторых устройствах исключено совместное использование флажков exclusive и background, например, у клавиатуры и мыши Совместный режим. Устройство может быть распределено по многим приложениям и не требует монопольного доступа.

NonExclusive NoWindowsKey Отключает клавишу «windows key» Для данного приложения мы можем использовать флажки foreground и non-exclusive. После входа в цикл при выполнении приложения мы пе­ реписываем состояние ввода устройства InputState и вызываем обработ­ чик событий DoEvents. Прежде чем записать код метода UpdatelnputState, необходимо создать текстовое поле с атрибутами мультистроки и «толь­ ко для чтения» и установить для него свойство Dock в значении «Fill». Теперь мы можем добавить следующий метод для изменения текстового окна textbox:

private void UpdatelnputState() { // Check the keys currently pressed first string pressedKeys = "Using GetPressedKeys(): \r\n";

foreach(Key k in device.GetPressedKeysf)) pressedKeys += k.ToString() + " ";

textBoxl.Text = pressedKeys;

} Перед запуском этого приложения осталось сделать две вещи. Следу­ ет обратить внимание на то, что цикл будет выполняться до тех пор, пока переменная running не получит значение «false», поэтому, если мы хо­ тим, чтобы выполнение приложения закачивалось при закрытии формы, необходимо присвоить переменной running значение «false»:

protected override void OnClosed(EventArgs e) { running = false;

} Это может помочь, когда мы вызываем метод инициализации InitializeInput. Изменим основной метод следующим образом:

Глава 15. Управление устройствами ввода static void Main() ( using (Forml frm = new FormlO) ( frm.Show();

frm.InitializelnputO ;

I } Теперь мы можем запустить это приложение. Нажимая и удерживая клавишу в любой момент времени, мы окажемся в текстовом поле. Обра­ тите внимание, что мы уже определили необходимые клавиши, не прибе­ гая к дополнительному коду. УДЕРЖИВАНИЕ НЕСКОЛЬКИХ КЛАВИШ Большинство клавиатур могут поддерживать нажатие до пяти кла­ виш одновременно. Нажатие большего числа клавиш приведет к иг­ норированию операций. Помимо описанного метода, использующего игровой цикл, для про­ верки изменения состояния устройства в Directlnput можно использо­ вать отдельный трэд или поток, работающий по принципу обработчика событий. Создайте новый метод инициализации ввода, листинг 15.3.

Листинг 15.3. Метод инициализация для Directlnput и Second Thread. private System.Threading.AutoResetEvent deviceUpdated;

private System.Threading.ManualResetEvent appShutdown;

public void InitializelnputWithThreadO { // Create our keyboard device device = new Device(SystemGuid.Keyboard);

device.SetCooperativeLevel(this, CooperativeLevelFlags.Background CooperativeLevelFlags.NonExclusive);

deviceUpdated = new System.Threading.AutoResetEvent(false) ;

appShutdown = new System.Threading.ManualResetEvent(false);

device.SetEventNotification(deviceUpdated);

System.Threading.Thread threadLoop = new System.Threading.Thread) new System.Threading.ThreadStart(this.ThreadFunction));

threadLoop.Start ();

device.Acquired ;

} Часть IV. Звук и устройства ввода Основная предпосылка этого метода такая же, как и в предыдущем, толь­ ко здесь мы объявили две переменные обработчика событий. Одна из них — AutoResetEvent — для Directlnput, другая — ManualResetEvent, чтобы уве­ домить трэд, когда приложение закрывается. Нам также понадобится об­ работчик ThreadFunction, отслеживающий одно из этих событий:

private void ThreadFunction() { System.Threading.WaitHandlef] handles = { deviceUpdated, appShutdown };

// Continue running this thread until the app has closed while(true) { int index = System.Threading.WaitHandle.WaitAny(handles);

if (index == 0) { UpdatelnputState();

} else if (index == 1) { return;

} } } Эта функция является паритетной, когда мы имеем дело с многопоточ­ ными приложениями. В случае если устройство Directlnput сообщает нам о событии изменения состояния устройства, мы вызываем метод UpdatelnputState;

если регистрируется другое событие, возвращается фун­ кция, которая прерывает поток. Как и в методе с использованием циклов, мы должны сделать еще две вещи, чтобы завершить написание приложе­ ния. Необходимо задать обработчик закрытия приложения при регистра­ ции события. Для этого замените перегрузку OnClosed на следующую:

protected override void OnClosed(EventArgs e) { if (appShutdown != null) appShutdown.Set();

} И мы должны модифицировать нашу основную процедуру, чтобы вызвать новый метод инициализации:

static void Main() { using (Forml frm = new Form()) Глава 15. Управление устройствами ввода frm.Show() ;

frm.InitializelnputWithThread();

Application.Run(frm);

} } В большинстве случаев нет необходимости получать список всех на­ жатых в данный момент клавиш;

допустим, нам захотелось обнаружить нажатие определенной клавиши. В этом сценарии мы получаем состоя­ ние клавиш и проверяем или сравниваем с тем состоянием, которое ищем. Например, чтобы увидеть, была ли нажата клавиша выхода «ESC», мож­ но сделать следующее:

KeyboardState state = device.GetCurrent KeyboardState();

if (state[Key.Escape]) { /* Escape was pressed */ } ОТКРЫТИЕ ПАНЕЛИ УПРАВЛЕНИЯ, ЗАВИСЯЩЕЙ ОТ КОНКРЕТНОГО УСТРОЙСТВА В классе Device имеется метод RunControlPanel, позволяющий откры­ вать панель управления в зависимости от выбранного устройства. На­ пример, при создании клавиатуры этот метод откроет панель со свой­ ствами клавиатуры;

для устройства мыши, откроет папку свойств мыши. Возможно внесение изменений в некоторые опции.

Использование устройства мыши Все устройства Directlnput используют один и тот же класс устройств Devices, так что различия между использованием мыши и клавиатуры весьма незначительные. Необходимо переписать метод создания устрой­ ства в коде Initializelnput(), чтобы использовать GUID идентификатор мыши вместо клавиатуры:

device = new Device(SystemGuid.Mouse);

ЭКСКЛЮЗИВНОЕ ИСПОЛЬЗОВАНИЕ МЫШИ При работе с мышью в эксклюзивном режиме в некоторых случаях мы можем полностью скрыть курсор мыши. Иногда это востребова­ но, но как правило, это не желательный эффект. Например, если вы пишете систему меню для вашей игры, которая работает в полно Часть IV. Звук и устройства ввода экранном режиме, пользователь может использовать это меню толь­ ко при помощи мыши. Необходимо предусмотреть для этого слу­ чая отдельные курсоры.

Для работы приложения нам необходимо изменить часть кода, где мы принимаем данные, поскольку для мыши эта операция теряет смысл. Перепишите метод UpdatelnputState следующим образом:

Pages:     | 1 |   ...   | 2 | 3 || 5 | 6 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.