Иван Андреев andreev.ia@gmail.com Основы 3Д. Освещение Целью данной работы является освоение принципов работы с источниками света и моделью освещения, предоставляемой BasicEffect. Задание на работу: Воспользовавшись библиотекой графических примитивов отобразить на экране несколько графических примитивов: куб, чайник и цилиндр. Задать два источника освещения с различными цветами. Задать режим попиксельного освещения. Теоретическая часть: Основной теоретический материал, необходимый для данного задания, подробно описан в лекционных и практических занятиях. Освещенность объекта в каждой точке вычисляется как сумма трех компонент: фонового, рассеянного и заркального света. Фоновая составляющая освещенности определяется по формуле: Ia=kaia 1 Ia – фоновая составляющая освещенности ka- коэффициент фонового освещения (свойство материала объекта воспринимать фоновое освещение) ia- интенсивность фонового освещения Фоновая составляющая освещенности определяется по формуле: N – нормально к поверхности L – направление к источнику света I d kd cos( L, N )id Id – рассеянная составляющая освещенности kd- коэффициент рассеянного освещения id- интенсивность рассеянного освещения Зеркальная составляющая освещенности определяется по формуле: N – нормально к поверхности L – направление к источнику света R – направление отраженного луча V – направление к наблюдателю 2 I s k s cos ( R,V )is Is – зеркальная составляющая освещенности ks- коэффициент зеркального освещения is- интенсивность зеркального освещения - коэффициент блеска (свойство материала) В идеале поверхность модели, например тора, является гладким объектом. Но чтобы работать с такой поверхностью требуется создать полигональную модель, которая является приближением с некоторой точностью идеального объекта. При этом вершины граней полигонального приближения принадлежат реальной поверхности. 3 Идеальная модель Полигональное приближение Для расчёта освещения необходимо задать нормали в тех точках, где оно будет вычисляться. Т.к. вершины полигонального приближения и вершины идеального объекта совпадают, то разумно задавать нормали в них. Другой способ - задание нормалей для граней. Такой способ приемлем, но он заведомо хуже, чем если бы нормали были заданы в вершинах. Действительно, вершина является точечным объектом, а грань имеет площадь. Т.о. для вершины можно задать нормаль точно, а для грани это некоторый усредненный вектор. К тому же, в моделях Гуро и Фонга нормаль должна быть задана в вершинах. Если исходно нормали были заданы для граней полигонов, то придется тем или иным образом восстанавливать их для вершин. Восстановленные значения будут чаще всего отличатся от реальных, так что некоторые детали будут потеряны. В качестве примера можно привести куб, нормали которого восстановлены усреднением по граням. 4 Нормали изначально заданы в вершинах Нормали восстановлены Конечно, это пример крайности, но он ярко демонстрирует потерю деталей при восстановлении. Прим. В некоторых случаях восстановление нормалей является необходимой частью построения полигональной модели, например, когда в качестве исходной информации есть только несвязный набор точек. Стоит отметить, что в DirectX и OpenGL нормали можно задавать только для вершин. Плоская модель затенения (flat shading) Идея алгоритма плоского затенения довольна простая. Сперва цвет вычисляется в каждой вершине треугольника, затем полученные значения 5 усредняются и весь треугольник закрашивается в полученный цвет. Данная модель обладает высокой скоростью работы, но на визуализированной модели чётко заметны переходы между гранями. Сфера с плоским затенением, около 2000 треугольников Сфера с плоским затенением, около 32000 треугольников Как видно на изображении, даже существенное увеличение количества треугольников не позволяет скрыть резкие переходы между ними. В настоящее время плоское затенение используется редко, в основном в программных визуализаторах или в тех случаях, когда необходимо подчеркнуть, что объект состоит из плоских граней. Прим. При использовании этой модели, не возникает трудностей, когда нормали заданы для граней, а не для вершин. В этом случае цвет треугольника можно рассчитывать в его геометрическом центре. Затенение по Гуро (Gouraud shading) В этой модели освещение не усредняется для грани, а линейно интерполируется между вершинами, поэтому для данной модели важно то, что нормали задаются в вершинах. Этот алгоритм позволяет получить гораздо более визуально-приятное изображение, чем при использовании алгоритма плоского затенения. 6 Сфера с затенением по Гуро, около 2000 треугольников Сфера с затенением по Гуро, около 32000 треугольников С другой стороны, вычислительная стоимость затенения по Гуро остается приемлемой, т.к. дорогостоящий расчёт освещения по-прежнему осуществляется в вершинах, а линейную интерполяцию можно хорошо оптимизировать. К сожалению, модель затенения по Гуро не безупречна. Блики на освещаемой поверхности при невысоком уровне детализации будут смазаны или могут вовсе "потеряться". Смазанный блик в модели затенения Гуро Чёткий блик Затенение по Фонгу (Phong shading) В этой модели между вершинами интерполируется не цвет, а нормаль. Цвет, в свою очередь, рассчитывается для каждого пикселя в отдельности. При использовании затенения по Фонгу изображение получается гораздо более качественным, чем при использовании предыдущих техник, и исчезает 7 проблема с бликами. Но данный алгоритм требует гораздо больше вычислительных ресурсов. К примеру, при использовании модели Фонга для визуализации сцены на экране с разрешением 1024x768, понадобится рассчитать освещение для более чем 700 000 точек. Сфера с затенением по Фонгу, около Сфера с затенением по Фонгу, около 2000 треугольников 32000 треугольников [http://www.compgraphics.info/3D/lighting/shading_model.php] Реализация: using using using using using using using using using using using using using System; System.Collections.Generic; System.Linq; Microsoft.Xna.Framework; Microsoft.Xna.Framework.Audio; Microsoft.Xna.Framework.Content; Microsoft.Xna.Framework.GamerServices; Microsoft.Xna.Framework.Graphics; Microsoft.Xna.Framework.Input; Microsoft.Xna.Framework.Media; Microsoft.Xna.Framework.Net; Microsoft.Xna.Framework.Storage; Primitives3D; namespace Lab6 { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; 8 SpriteBatch spriteBatch; BasicEffect effect; TeapotPrimitive teapot; CylinderPrimitive cylinder; CubePrimitive cube; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // TODO: Add your initialization logic here effect = new BasicEffect(GraphicsDevice, null); cube = new CubePrimitive(GraphicsDevice); cylinder = new CylinderPrimitive(GraphicsDevice); teapot = new TeapotPrimitive(GraphicsDevice); base.Initialize(); } /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); // TODO: use this.Content to load your game content here } /// <summary> /// UnloadContent will be called once per game and is the place to unload /// all content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> 9 protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // TODO: Add your update logic here base.Update(gameTime); } /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: Add your drawing code here Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds); Matrix view = Matrix.CreateLookAt(new Vector3(0, 1, 5), Vector3.Zero, Vector3.Up); Matrix proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10); effect.View = view; effect.Projection = proj; effect.LightingEnabled = true; effect.SpecularColor = new Vector3(1, 1, 1); effect.SpecularPower = 24; effect.AmbientLightColor = new Vector3(0.2f, 0.2f, 0.2f); // Задание параметров первого источника света effect.DirectionalLight0.Enabled = true; effect.DirectionalLight0.Direction = new Vector3(1, 0, 0); effect.DirectionalLight0.DiffuseColor = new Vector3(0.5f, 0, 0); effect.DirectionalLight0.SpecularColor = new Vector3(1, 0, 0); // Задание параметров второго источника света effect.DirectionalLight1.Enabled = true; effect.DirectionalLight1.Direction = new Vector3(-1, 0, 0); effect.DirectionalLight1.DiffuseColor = new Vector3(0, 0, 0.5f); effect.DirectionalLight1.SpecularColor = new Vector3(0, 0, 1); // Включение попиксельного освещения effect.PreferPerPixelLighting = true; effect.World = world; effect.DiffuseColor = new Vector3(1, 0, 0); cube.Draw(effect); effect.World = world * Matrix.CreateTranslation(2, 0, 0); effect.DiffuseColor = new Vector3(0, 1, 0); cylinder.Draw(effect); effect.World = world * Matrix.CreateTranslation(-2, 0, 0); 10 effect.DiffuseColor = new Vector3(0, 0, 1); teapot.Draw(effect); base.Draw(gameTime); } } } 11