Основы разработки компьютерных игр в XNA Game Studio

Анимация, эффекты

Показывать лекцию целиком

Примеры к лабораторной работе

Задачи работы

  • Рассмотреть основы анимации трехмерных персонажей
  • Рассмотреть применение графических эффектов в компьютерных играх, созданных на базе XNA
  • Анимация персонажей

    Компьютерные игры наполнены анимированными персонажами. XNA содержит встроенные средства, поддерживающие базовые операции по анимации трехмерных персонажей. В частности, это средства для работы с так называемым скелетом персонажа. Скелет состоит из взаимосвязанных костей, управляя которыми можно анимировать модель. Создание анимированных моделей – это отдельная, достаточно трудоемкая задача, которая требует серьезных познаний в области 3D-моделирования. Для подготовки анимированных трехмерных моделей вы можете использовать практически любой 3D-редактор.

    XNA содержит лишь базовые средства для управления скелетами объектов. Так, мы можем получить коллекцию костей объекта и проводить с ними какие-либо преобразования, вызывающие движение модели. Однако, такой подход ограничен – с его использованием весьма сложно создать реалистичное движение для сложных моделей. На основе базовых средств XNA разработаны библиотеки кода, которые содержат компоненты, позволяющие анимировать персонажи на более высоком уровне. В частности, одну из таких библиотек – XNAAnimation – можно найти на сайте http://www.codeplex.com/xnanimation.

    Здесь мы рассмотрим базовый подход к анимации модели. Он заключается в следующем. Можно менять параметры отдельных костей модели и выводить ее на экран с учетом этих изменений. Мы использовали бесплатную модель .

    Создадим новый проект P18_1. Загрузим в него модель Ballpen.fbx. На рис. 24.1. представлено окно Solution Explorer нашего нового проекта.

    (рис 24.1) Окно Project Explorer

    Код проекта сосредоточен в классе Game1.cs, его код вы можете найти в листинге 24.1.

    using System;
    using System.Collections.Generic;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Audio;
    using Microsoft.Xna.Framework.Content;
    using Microsoft.Xna.Framework.GamerServices;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;
    using Microsoft.Xna.Framework.Net;
    using Microsoft.Xna.Framework.Storage;
    
    namespace P18_1
    {
        public class Game1 : Microsoft.Xna.Framework.Game
        {
            GraphicsDeviceManager graphics;
            //Матрицы
            Matrix viewMatrix;
            Matrix projMatrix;
            //Модель
            Model pen;
            public Game1()
            {
                graphics = new GraphicsDeviceManager(this);
                Content.RootDirectory = "Content";
            }
    
            protected override void Initialize()
            {
                base.Initialize();
            }
    
            protected override void LoadContent()
            {
                //Загрузка модели
                pen = Content.Load<Model>("ballpen");
                //Соотношение сторон
                float aspectRatio = (float)graphics.GraphicsDevice.Viewport.Width /
                   (float)graphics.GraphicsDevice.Viewport.Height;
                //Устанавливаем камеру
                viewMatrix = Matrix.CreateLookAt(new Vector3(0, 30, 80), new Vector3(-32, 0, 0), new Vector3(0, 1, 0));
                //Устанавливаем проекционную матрицу
                projMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45.0f), aspectRatio, 1.0f, 1000.0f);
            }
    
            protected override void UnloadContent()
            {
                
            }
    
            protected override void Update(GameTime gameTime)
            {
                //Перемещение частей ручки
                PenMove();
                base.Update(gameTime);
            }
            //Процедура для перемещения частей ручки
            void PenMove()
            {
                //Получить состояние клавиатуры
                KeyboardState keyboardState = Keyboard.GetState();
                //Поворот зажима - он представлен родительской костью сети №3
                //Умножим его матрицу трансформации на матрицу поворота по оси X
                pen.Meshes[3].ParentBone.Transform = pen.Meshes[3].ParentBone.Transform * Matrix.CreateRotationX
      (MathHelper.ToRadians(-1.0f));
                //Кнопка ручки перемещается по клавиатурным командам
                //При нажатии кнопки "Влево"
                //Она выходит из ручки
                if (keyboardState.IsKeyDown(Keys.Left))
                {
                    //Умножим матрицу трансформации родительской костью сети №1
                    //На матрицу трансляции, перемещающей ее на 2 позиции влево по оси Х
                    pen.Meshes[1].ParentBone.Transform = pen.Meshes[1].ParentBone.Transform * Matrix.CreateTranslation
      (new Vector3(-2, 0, 0));
                }
                //При нажатии кнопки "Вправо"
                //Кнопка входит в ручку
                if (keyboardState.IsKeyDown(Keys.Right))
                {
                    //Умножим матрицу трансформации родительской кости сети №1
                    //На матрицу трансляции, перемещающую ее на 2 позиции по оси X вправо
                    pen.Meshes[1].ParentBone.Transform = pen.Meshes[1].ParentBone.Transform * Matrix.CreateTranslation
      (new Vector3(2, 0, 0));
                }
    
            }
            protected override void Draw(GameTime gameTime)
            {
                graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
                //Новый массив матриц размером, соответствующим количеству
                //костей в скелете модели
                Matrix[] absoluteTransformations = new Matrix[pen.Bones.Count];
                //Скопировать матрицы трансформации костей в массив матриц
                pen.CopyAbsoluteBoneTransformsTo(absoluteTransformations);
    
                foreach (ModelMesh mesh in pen.Meshes)
                {
                    foreach (BasicEffect effect in mesh.Effects)
                    {
                        effect.LightingEnabled = true;
                        effect.EnableDefaultLighting();
                        effect.Projection = projMatrix;
                        effect.View = viewMatrix;
                        //Установим новую мировую матрицу для родительской кости текущей сети
                        //это приводит к перемещению кнопки и к перемещению зажима
                        //Так же здесь мы уменьшаем модель, применяя коэффициент масштабирования 0,13
                        effect.World = absoluteTransformations[mesh.ParentBone.Index] * Matrix.CreateScale(0.13f);
                    }
                    //Выводим подготовленную сеть
                    mesh.Draw();
                }
                base.Draw(gameTime);
            }
        }
    }

    На рис. 24.2. вы можете видеть игровой экран проекта P18_1.

    (рис 24.2) Игровой экран проекта P18_1

    Эффекты

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

    Шейдерные программы или просто шейдеры представлены в виде файлов с расширением FX. Эти файлы можно создавать как с помощью специального ПО для разработки и отладки шейдеров, так и вручную, в редакторе кода Visual Studio. Для описания шейдерных программ существует специальный язык – HLSL – High Level Shader Language – Высокоуровневый язык описания шейдеров.

    Шейдерные программы исполняются графическим процессором видеокарты, с их помощью можно создать множество графических эффектов, которыми наполнены современные игры.

    Шейдеры принято делить на вершинные (Vertex Shader) и пиксельные (Pixel Shader).

    Вершинные работают с вершинами объектов. С их помощью можно осуществить такие операции, как деформацию объектов, анимацию, перемещение и т.д. После того, как вершинный шейдер завершит работу по модификации модели, за дело принимается пиксельный шейдер.

    Пиксельные шейдеры применяются к отдельным пикселям изображения. В частности, они отвечают за цвет пикселей, позволяют применять к изображению различные эффекты.

    Для того, чтобы создать новый FX-файл в XNA-проект, достаточно выполнить щелчок левой кнопкой мыши по папке Content, выбрать пункт Add $$\Rightarrow$$ New Item и в появившемся окне выбрать в качестве типа добавляемого файла Effect File. После того, как файл эффектов будет добавлен в проект, он будет содержать некоторые стандартные части, которые представляют собой объявление переменных, необходимых для работы шейдера, объявление точки входа в шейдер и, собственно, шаблоны двух шейдеров – вершинного и пиксельного.

    Для того, чтобы разрабатывать шейдеры самостоятельно с помощью XNA-редактора FX-файлов, вам нужно ознакомиться с языком HLSL. Вы можете сделать это, воспользовавшись справочной службой MSDN, в частности, этим разделом: http://msdn2.microsoft.com/en-us/library/bb509561.aspx.

    На практике лучше всего пользоваться специализированным ПО для разработки и отладки шейдеров.

    Рассмотрим пример работы с шейдерными эффектами – он создан с использованием примера применения шейдеров, приведенного в документации к XNA.

    Создадим новый проект P18_2. Нарис. 24.3. вы можете видеть его окно Solution Explorer.

    (рис 24.3) Окно Solution Explorer для проекта P18_2

    Файл shader.fx содержит код шейдера, файл tex.png используется в качестве текстуры для наложения на модель, созданную программными средствами. Код программы реализован в классе Game1. Рассмотрим его (листинг. 24.2.).

    using System;
    using System.Collections.Generic;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Audio;
    using Microsoft.Xna.Framework.Content;
    using Microsoft.Xna.Framework.GamerServices;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;
    using Microsoft.Xna.Framework.Net;
    using Microsoft.Xna.Framework.Storage;
    
    namespace P18_2
    {
        public class Game1 : Microsoft.Xna.Framework.Game
        {
            GraphicsDeviceManager graphics;
            //Матрицы
            Matrix WorldMAtrix, ViewMatrix, ProjectionMatrix;
            //Объект для применения эффектов
            Effect effect;
            //Вершины куба
            VertexDeclaration cubeVertexDeclaration;
            //Координаты для наложения текстур
            VertexPositionTexture[] cubeVertices;
            //Вершинный буфер
            VertexBuffer vertexBuffer;
            //Индексный буфер
            IndexBuffer indexBuffer;
            //Массив индексов
            short[] cubeIndices;
            //Номер эффекта в шейдере
            int CurPass;
            //Множитель для текстуры
            float Multiplier;
            //Текстура
            Texture2D texture;
    
            public Game1()
            {
                graphics = new GraphicsDeviceManager(this);
                Content.RootDirectory = "Content";
            }
    
            protected override void Initialize()
            {
                // TODO: Add your initialization logic here
    
                base.Initialize();
            }
    
            
            protected override void LoadContent()
            {
                //Мировая матрица
                WorldMAtrix = Matrix.Identity;
                //Матрица вида
                ViewMatrix  = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero,
                    Vector3.Up);
                //Матрица проекции
                ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                    MathHelper .ToRadians (45.0f),
                    (float)graphics.GraphicsDevice.Viewport.Width /
                    (float)graphics.GraphicsDevice.Viewport.Height,
                    1.0f, 10.0f);
                //Текущий эффект имеет номер 0
                CurPass = 0;
                //Текущий множитель - 1
                Multiplier = 1;
                //Загрузим текстуру
                texture = Content.Load<Texture2D>("tex");
                //Загрузим шейдер shader в переменную типа Effect
                effect = Content.Load<Effect>("shader");
                //Передадим в переменную шейдера UserTexture текстуру
                effect.Parameters["UserTexture"].SetValue(texture);
                //Установим переменную шейдера Multiplier в значение
                //соответствующей игровой переменной
                effect.Parameters["Multiplier"].SetValue(Multiplier);
                //Создадим куб
                InitializeModel();
            }
    
            protected override void UnloadContent()
            {
                // TODO: Unload any non ContentManager content here
            }
            /// <summary>
            /// Создаем 3D-модель
            /// </summary>
            public void InitializeModel()
            {
                // Переменная типа VertexDeclaration
                cubeVertexDeclaration = new VertexDeclaration(
                    graphics.GraphicsDevice, VertexPositionTexture.VertexElements);
    
                //Установка параметров точек, которые будут использованы для рисования фигуры
                Vector3 topLeftFront = new Vector3(-1.0f, 1.0f, 1.0f);
                Vector3 bottomLeftFront = new Vector3(-1.0f, -1.0f, 1.0f);
                Vector3 topRightFront = new Vector3(1.0f, 1.0f, 1.0f);
                Vector3 bottomRightFront = new Vector3(1.0f, -1.0f, 1.0f);
                Vector3 topLeftBack = new Vector3(-1.0f, 1.0f, -1.0f);
                Vector3 topRightBack = new Vector3(1.0f, 1.0f, -1.0f);
                Vector3 bottomLeftBack = new Vector3(-1.0f, -1.0f, -1.0f);
                Vector3 bottomRightBack = new Vector3(1.0f, -1.0f, -1.0f);
    
                // Координаты текстуры
                Vector2 textureTopLeft = new Vector2(0.0f, 0.0f);
                Vector2 textureTopRight = new Vector2(1.0f, 0.0f);
                Vector2 textureBottomLeft = new Vector2(0.0f, 1.0f);
                Vector2 textureBottomRight = new Vector2(1.0f, 1.0f);
    
                // Массив для хранения списка вершин
                // Он используется для передачи данных в вершинный буфер
                cubeVertices = new VertexPositionTexture[36];
    
                // Передняя часть фигуры
                cubeVertices[0] =
                    new VertexPositionTexture(
                    topLeftFront, textureTopLeft); // 0
                cubeVertices[1] =
                    new VertexPositionTexture(
                    bottomLeftFront, textureBottomLeft); // 1
                cubeVertices[2] =
                    new VertexPositionTexture(
                    topRightFront, textureTopRight); // 2
                cubeVertices[3] =
                    new VertexPositionTexture(
                    bottomRightFront, textureBottomRight); // 3
    
                // Задняя часть фигуры
                cubeVertices[4] =
                    new VertexPositionTexture(
                    topLeftBack, textureTopRight); // 4
                cubeVertices[5] =
                    new VertexPositionTexture(
                    topRightBack, textureTopLeft); // 5
                cubeVertices[6] =
                    new VertexPositionTexture(
                    bottomLeftBack, textureBottomRight); //6
                cubeVertices[7] =
                    new VertexPositionTexture(
                    bottomRightBack, textureBottomLeft); // 7
    
                // Верхняя часть фигуры
                cubeVertices[8] =
                    new VertexPositionTexture(
                    topLeftFront, textureBottomLeft); // 8
                cubeVertices[9] =
                    new VertexPositionTexture(
                    topRightBack, textureTopRight); // 9
                cubeVertices[10] =
                    new VertexPositionTexture(
                    topLeftBack, textureTopLeft); // 10
                cubeVertices[11] =
                    new VertexPositionTexture(
                    topRightFront, textureBottomRight); // 11
    
                // Нижняя часть фигуры
                cubeVertices[12] =
                    new VertexPositionTexture(
                    bottomLeftFront, textureTopLeft); // 12
                cubeVertices[13] =
                    new VertexPositionTexture(
                    bottomLeftBack, textureBottomLeft); // 13
                cubeVertices[14] =
                    new VertexPositionTexture(
                    bottomRightBack, textureBottomRight); // 14
                cubeVertices[15] =
                    new VertexPositionTexture(
                    bottomRightFront, textureTopRight); // 15
    
                // Левая часть фигуры
                cubeVertices[16] =
                    new VertexPositionTexture(
                    topLeftFront, textureTopRight); // 16
                cubeVertices[17] =
                    new VertexPositionTexture(
                    bottomLeftFront, textureBottomRight); // 17
                cubeVertices[18] =
                    new VertexPositionTexture(
                    topRightFront, textureTopLeft); // 18
                cubeVertices[19] =
                    new VertexPositionTexture(
                    bottomRightFront, textureBottomLeft); // 19
    
                //Создаем вершинный буфер для хранения информации о вершинах
                vertexBuffer = new VertexBuffer(graphics.GraphicsDevice,
                    VertexPositionTexture.SizeInBytes * cubeVertices.Length,
                    BufferUsage.None
                    );
    
                //Добавляем данные в вершинный буфер
                vertexBuffer.SetData<VertexPositionTexture>(cubeVertices);
    
                // С помощью этого массива определяем, к каким частям фигуры
                //Относятся те или иные компоненты массива cubeVertices
                cubeIndices = new short[] {
                                         0,  1,  2,  // Передняя плоскость
                                         1,  3,  2,
                                         4,  5,  6,  // Задняя плоскость
                                         6,  5,  7,
                                         8,  9, 10,  // Верхняя плоскость
                                         8, 11,  9,
                                        12, 13, 14,  // Нижняя плоскость
                                        12, 14, 15,
                                        16, 13, 17,  // Левая плоскость
                                        10, 13, 16,
                                        18, 19, 14,  // Правая плоскость
                                         9, 18, 14 };
    
                //Индексный буфер
                indexBuffer = new IndexBuffer(graphics.GraphicsDevice,
                    sizeof(short) * cubeIndices.Length,
                    BufferUsage.None,
                    IndexElementSize.SixteenBits
                    );
    
                //Добавляем данные в индексный буфер
                indexBuffer.SetData<short>(cubeIndices);
    
            }
    
            
            protected override void Update(GameTime gameTime)
            {
                //Получим состояние клавиатуры
                KeyboardState Key = Keyboard.GetState();
                //Если нажата клавиша 1 - установить
                //номер прохода эффекта в 0
                if (Key.IsKeyDown (Keys.D1))
                {
                    CurPass = 0;
                }
                //Если нажата клавиша 2 - 
                //установить номер прохода в 1
                if (Key.IsKeyDown(Keys.D2))
                {
                    CurPass = 1;
                }
                //Если нажата клавиша 3 
                //установить номер прохода в 2
                if (Key.IsKeyDown(Keys.D3))
                {
                    CurPass = 2;
                }
                //По нажатию клавиш QWERT
                //Устанавливать различные множители
                //они влияют на размеры текстуры, размещенных
                //на гранях куба
                //Например, если Multiplier=1 - лишь один экземпляр
                //изображения текстуры располагается на грани
                //Если Multiplier=2 - на грани размещается уже
                //4 текстуры и т.д.
                if (Key.IsKeyDown(Keys.Q))
                {
                    Multiplier = 10.0f;
                }
                if (Key.IsKeyDown(Keys.W))
                {
                    Multiplier = 5.0f;
                }
                if (Key.IsKeyDown(Keys.E))
                {
                    Multiplier = 2.0f;
                }
                if (Key.IsKeyDown(Keys.R))
                {
                    Multiplier = 1.0f;
                }
                if (Key.IsKeyDown(Keys.T))
                {
                    Multiplier = 0.5f;
                }
                //Модифицируем мировую матрицу таким образом, чтобы модель
                //поворачивалась в пространстве с небольшой скоростью
                WorldMAtrix = WorldMAtrix * Matrix.CreateRotationX(0.011f) * 
                    Matrix.CreateRotationY(0.012f)*
                    Matrix.CreateRotationZ (0.013f);
                //Установить матрицу, которая является результатом умножения мировой, видовой и 
                //проекционной матриц в эффекте
                effect.Parameters["WorldViewProj"].SetValue(WorldMAtrix *ViewMatrix *ProjectionMatrix);
                //Установить переменную Multiplier в эффекте
                effect.Parameters["Multiplier"].SetValue(Multiplier);
                base.Update(gameTime);
            }
    
            
            protected override void Draw(GameTime gameTime)
            {
                //Очистить экран
                graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
                //Выключить режим удаления задних частей треугольников
              graphics.GraphicsDevice.RenderState.CullMode =
                    CullMode.None;
                //Установить матрицу вершин
                graphics.GraphicsDevice.VertexDeclaration = cubeVertexDeclaration;
                //Установить матрицу индексов
                graphics.GraphicsDevice.Indices = indexBuffer;
                //Установить вершинный буфер
                graphics.GraphicsDevice.Vertices[0].SetSource(
                    vertexBuffer,
                    0,
                    VertexPositionTexture.SizeInBytes);
                //Начало работы эффекта, используемого для вывода изображения
                effect.Begin();
                    //Использовать проход шейдера, заданный переменной CurPass
                    effect.CurrentTechnique.Passes[CurPass].Begin();
                        //Вывести изображение как набор индексированных 
                        //примитивов, используя настройки буферов, сделанные ранее
                        graphics.GraphicsDevice.DrawIndexedPrimitives(
                            PrimitiveType.TriangleList, 
                            0,
                            0,
                            cubeVertices.Length,
                            0,
                            12);
                    //Завершить текущий проход шейдера
                    effect.CurrentTechnique.Passes[CurPass].End();
                 //Завершить вывод изображений
                 effect.End();
                base.Draw(gameTime);
            }
        }
    }

    Теперь рассмотрим код шейдера – в листинге 24.3. вы можете найти код файла shader.fx.

    //Переменные, устанавливаемые при настройке шейдера
    //Матрица - результат перемножений мировой, видовой, проекционной матриц
    uniform extern float4x4 WorldViewProj : WORLDVIEWPROJECTION;
    //Текстура
    uniform extern texture UserTexture;
    //Множитель
    uniform extern float Multiplier;
    
    struct VS_OUTPUT
    {
        float4 position  : POSITION;
        float4 textureCoordinate : TEXCOORD0;
    };
    
    sampler textureSampler = sampler_state
    {
    	Texture = <UserTexture>;
    	mipfilter = LINEAR; 
    };
    //Вывод вершин текстуры
    //Вершинный шейдер, который используется
    //во всех вариантах эффекта
    VS_OUTPUT Transform(
        float4 Position  : POSITION, 
        float4 TextureCoordinate : TEXCOORD0 )
    {
        VS_OUTPUT Out = (VS_OUTPUT)0;
        Out.position = mul(Position, WorldViewProj);
        Out.textureCoordinate = TextureCoordinate*Multiplier;
        return Out;
    }
    
    //Вывод текстуры без изменений для P0, пиксельный шейдер
    //Возврат того же цвета, который был передан
    //шейдеру
    float4 ApplyTexture(VS_OUTPUT vsout) : COLOR
    {
    	return tex2D(textureSampler, vsout.textureCoordinate).rgba;
    }
    
    
    //Вывод размытой текстуры для P1, пиксельный шейдер
    //Цвет устанавливается исходя из цвета
    //соседних пикселей
    //Полученная цветовая информация делится на 5
    //для сохранения исходной яркости
    float4 ApplyTextureBlur(VS_OUTPUT vsout) : COLOR
    {
    	float4 Col;
        Col =  tex2D(textureSampler, vsout.textureCoordinate);
    	Col += tex2D(textureSampler, vsout.textureCoordinate + (0.01));
    	Col += tex2D(textureSampler, vsout.textureCoordinate + (0.02));
    	Col += tex2D(textureSampler, vsout.textureCoordinate + (0.03));
    	Col += tex2D(textureSampler, vsout.textureCoordinate + (0.04));
    	Col = Col / 5;
    	//Возвращаем найденный цвет пикселя
    	return Col;
    }
    //Вывод текстуры в оттенках серого для P2 - пиксельный шейдер
    //Интенсивность света всех каналов изображения складывается и 
    //делится на количество каналов
    //Альфа-канал устанавливается равным 1
    float4 ApplyTextureGray(VS_OUTPUT vsout) : COLOR
    {
    	float4 Col;
        Col =  tex2D(textureSampler, vsout.textureCoordinate);
        Col.a=1.0f;
        Col.rgb=(Col.r+Col.g+Col.b)/3;
        return Col;
    }
    
    //Техника шейдера
    technique TransformAndTexture
    {
    	//Проход P0, имеет индекс 0
        pass P0
        {
    		//Вершинный шейдер для данного прохода
            vertexShader = compile vs_2_0 Transform();
            //Пиксельный шейдер для данного прохода
            pixelShader  = compile ps_2_0 ApplyTexture();
        }
        //Проход P1, имеет индекс 1
        pass P1
        {
    		vertexShader = compile vs_2_0 Transform();
            pixelShader  = compile ps_2_0 ApplyTextureBlur();
        }
        //Проход P2, имеет индекс 2
        pass P2
        {
    		vertexShader = compile vs_2_0 Transform();
            pixelShader  = compile ps_2_0 ApplyTextureGray();
        }
    }

    На рис. 24.4., 24.5., 24.6. вы можете видеть игровое окно проекта в различных режимах применения шейдера.

    (рис 24.5) Наложение текстуры без изменения цвета, с множителем 2(рис 24.4) Наложение текстуры с размытием, множитель 1(рис 24.6) Текстура в оттенках серого, множитель 1

    Семинар №2. Защита итоговых проектов

    Данное семинарское занятие посвящено защите итоговых проектов учащихся.

    Для проведения семинара необходимо презентационное оборудование.

    Вернуться к учебному плану