Зрабіце ўсплёск з дынамічнымі 2D-эфектамі вады
Belarusian (беларуская мова) translation by Jenyia (you can also view the original English article)
Sploosh! У гэтым уроку я пакажу вам, як вы можаце выкарыстоўваць простыя матэматычныя, фізічныя і эфекты часціц, каб імітаваць цудоўныя 2D хвалі і кроплі вады.
Заўвага: Хоць гэты падручнік напісаны з выкарыстаннем C# і XNA, вы павінны выкарыстоўваць адны і тыя ж метады і канцэпцыі практычна ў любой гульнявой асяроддзі распрацоўкі.
Папярэдні прагляд вынікаў
Калі ў вас XNA, вы можаце загрузіць зыходныя файлы і скампіляваць дэма. У адваротным выпадку азнаёмцеся з дэма-відэа ніжэй:
У воднай сімуляцыі ёсць дзве ў асноўным незалежныя часткі. Па-першае, мы будзем выкарыстоўваць хвалі з выкарыстаннем спружыннай мадэлі. Па-другое, мы будзем выкарыстоўваць эфекты часціц для дадання воплескаў.
Стварэнне хваляў
Каб зрабіць хвалі, мы будзем мадэляваць паверхню вады як шэраг вертыкальных спружын, як паказана на гэтай дыяграме:



Гэта дазволіць хвалям падымацца і апускацца. Затым мы працягнем часціцы вады на суседнія часціцы, каб хвалі распаўсюдзіліся.
Спружыны і закон Гука
Адна з вялікіх асаблівасцяў спружын заключаецца ў тым, што іх лёгка імітаваць. Спружыны маюць пэўную натуральную даўжыню; калі вы расцягнецца або сціскаеце спружыну, ён паспрабуе вярнуцца да гэтай натуральнай даўжыні.
Сіла, якая забяспечваецца спружынай, дадзена Законам Гука:
\[
F = -kx
\]
F
- сіла, якая ствараецца спружынай, k
- пастаянная спружына, а x
- зрушэнне спружын ад яе натуральнай даўжыні. Адмоўны знак паказвае на тое, што сіла знаходзіцца ў процілеглым кірунку, да якога спружына зрушаная; калі вы націснеце спружыну ўніз, яна будзе адціскацца, і наадварот.
Пастаянная спружына, k
, вызначае жорсткасць спружыны.
Каб імітаваць спружыны, мы павінны высветліць, як перамяшчаць часціцы вакол па закону Гука. Для гэтага нам трэба яшчэ некалькі формул з фізікі. Па-першае, другі закон руху Ньютана:
\[
F = ma
\]
Тут F
- сіла, m
- маса, a
- паскарэнне. Гэта азначае, што сіла мацней насоўваецца на аб'ект, і чым лягчэй аб'ект, тым больш ён паскараецца.
Аб'яднанне гэтых двух формул і пераўпарадвачэнне дае нам:
\[
a = -\frac{k}{m} x
\]
Гэта дае нам паскарэнне для нашых часціц. Будзем лічыць, што ўсе нашы часціцы будуць мець аднолькавую масу, таму мы можам аб'яднаць k/m
ў адну канстанту.
Каб вызначыць становішча ад паскарэння, нам трэба правесці колькаснае інтэграванне. Мы збіраемся выкарыстоўваць найпростую форму колькаснага інтэгравання - з кожным кадрам мы проста робім наступнае:
Position += Velocity; Velocity += Acceleration;
Гэта называецца метадам Эйлера. Гэта не самы дакладны тып лікавай інтэграцыі, але ён хуткі, просты і адэкватны для нашых мэтаў.
Злучаючы ўсё гэта разам, нашы часціцы на паверхні вады будуць рабіць наступныя кадры:
public float Position, Velocity; public void Update() { const float k = 0.025f; // adjust this value to your liking float x = Height - TargetHeight; float acceleration = -k * x; Position += Velocity; Velocity += acceleration; }
Тут TargetHeight
з'яўляецца натуральным становішчам верхняй частцы спружыны, калі ён не расцягваецца і не сціскаецца. Вы павінны ўсталяваць гэта значэнне там, дзе вы хочаце, каб была паверхня вады. Вы павінны ўсталяваць гэта значэнне там, дзе вы хочаце, каб была паверхня вады.
Нацяжэнне і Ўвільгатненне
Я згадаў раней, што пастаянная спружына k
рэгулюе жорсткасць спружыны. Вы можаце адрэгуляваць гэтае значэнне, каб змяніць ўласцівасці вады. Нізкая пастаянная спружыны прымусіць спружыну вызваліцца. Гэта азначае, што сіла прымусіць вялікія хвалі вагацца павольна. І наадварот, высокая пастаянная спружына павялічыць нацяжэнне спружыны. Сілы створаць невялікія хвалі, якія хутка вагаюцца. Высокая пастаянная спружына прымусіць ваду пахадзіць на змешванне Джелло.
Папярэджанне: не ўстанаўлівайце занадта высокую пастаянную спружыну. Вельмі жорсткія спружыны ўжываюць вельмі моцныя сілы, якія моцна змяняюцца за вельмі малы прамежак часу. Гэта не вельмі добра спалучаецца з лікавым інтэграваннем, якое імітуе спружыны як серыю дыскрэтных скокаў з рэгулярнымі часовымі інтэрваламі. Вельмі жорсткая спружына можа нават мець перыяд ваганняў, які карацей вашага часовага кроку. Горш таго, метад інтэграцыі Эйлера мае тэндэнцыю да павелічэння энергіі, паколькі імітацыя становіцца менш дакладнай, што прыводзіць да выбуху жорсткіх спружын.
Да гэтага часу праблема з нашай спружыннай мадэллю. Як толькі спружына пачне вагацца, яна ніколі не спыніць. Каб вырашыць гэтую праблему, мы павінны ўжыць некаторы ўвільгатненне. Ідэя складаецца ў тым, каб прымяніць сілу ў процілеглым кірунку, каб наша спружына рухалася, каб запаволіць яе. Гэта патрабуе невялікі карэкціроўкі нашай вясновай формулы:
\[
a = -\frac{k}{m} x - dv
\]
Тут v
- хуткасць, d
- каэфіцыент згасання - іншая канстанта, якую вы можаце наладзіць, каб наладзіць адчуванне вады. Гэта павінна быць даволі мала, калі вы хочаце, каб вашы хвалі вагаліся. Дэма выкарыстоўвае каэфіцыент згасання 0,025. Высокі каэфіцыент згасання прымусіць ваду выглядаць тоўстай, як меласы, у той час як нізкае значэнне дазволіць ваганняў хваляў на працягу доўгага часу.
Прасоўванне хваляў
Цяпер, калі мы можам зрабіць спружыну, давайце выкарыстоўваць іх для мадэлявання вады. Як паказана на першай дыяграме, мы мадэлюем ваду, выкарыстоўваючы шэраг паралельных вертыкальных спружын. Вядома, калі спружыны ўсе незалежныя, хвалі ніколі не будуць распаўсюджвацца, як гэта робяць сапраўдныя хвалі.
Спачатку я пакажу код, а потым перайду да яго:
for (int i = 0; i < springs.Length; i++) springs[i].Update(Dampening, Tension); float[] leftDeltas = new float[springs.Length]; float[] rightDeltas = new float[springs.Length]; // do some passes where springs pull on their neighbours for (int j = 0; j < 8; j++) { for (int i = 0; i < springs.Length; i++) { if (i > 0) { leftDeltas[i] = Spread * (springs[i].Height - springs [i - 1].Height); springs[i - 1].Speed += leftDeltas[i]; } if (i < springs.Length - 1) { rightDeltas[i] = Spread * (springs[i].Height - springs [i + 1].Height); springs[i + 1].Speed += rightDeltas[i]; } } for (int i = 0; i < springs.Length; i++) { if (i > 0) springs[i - 1].Height += leftDeltas[i]; if (i < springs.Length - 1) springs[i + 1].Height += rightDeltas[i]; } }
Гэты код будзе выклікацца кожны кадр з вашага метаду Update()
. Тут springs
ўяўляюць сабой масіў спружын, размешчаных злева направа. leftDelta
s - гэта масіў паплаўкоў, які захоўвае розніцу ў вышыні паміж кожнай спружынай і яе левым суседам. rightDeltas
з'яўляецца эквівалентам для правых суседзяў. Мы захоўваем ўсе гэтыя рознасці вышынь у масівах, таму што апошнія два аператара if
змяняюць вышыню спружын. Мы павінны вымяраць розніцу вышынь да таго, як якая-небудзь з вышынь будзе змененая.
Код пачынаецца з запуску закона Гука па кожнай спружыне, як апісана вышэй. Затым ён глядзіць на розніцу вышынь паміж кожнай спружынай і яе суседзямі, і кожная спружына цягне свае суседнія спружыны да сябе, змяняючы становішча і хуткасць суседзяў. Суседні-працягвальны крок паўтараецца восем разоў, каб хвалі распаўсюджваліся хутчэй.
Існуе адно больш наладжвальных значэнне, тут называецца Spread
. Ён кантралюе хуткасць распаўсюджвання хваль. Яно можа прымаць значэнні ад 0 да 0,5, пры гэтым вялікія значэння павялічваюць хуткасць распаўсюджвання хваль.
Каб пачаць рух хваляў, мы дададзім просты метад Splash()
.
public void Splash(int index, float speed) { if (index >= 0 && index < springs.Length) springs[i].Speed = speed; }
Кожны раз, калі вы хочаце зрабіць хвалі, выклічце Splash()
. Параметр index
вызначае, на якой спружыне павінен з'явіцца ўсплёск, а параметр seed
вызначае, наколькі вялікія будуць хвалі.
Рэндэрынг
Мы будзем выкарыстоўваць клас XNA PrimitiveBatch
з XNA PrimitivesSample. Клас PrimitiveBatch
дапамагае нам маляваць лініі і трыкутнікі непасрэдна з графічным працэсарам. Вы выкарыстоўваеце яго так:
// in LoadContent() primitiveBatch = new PrimitiveBatch(GraphicsDevice); // in Draw() primitiveBatch.Begin(PrimitiveType.TriangleList); foreach (Triangle triangle in trianglesToDraw) { primitiveBatch.AddVertex(triangle.Point1, Color.Red); primitiveBatch.AddVertex(triangle.Point2, Color.Red); primitiveBatch.AddVertex(triangle.Point3, Color.Red); } primitiveBatch.End();
Варта адзначыць, што па змаўчанні вы павінны ўказваць вяршыні трохвугольніка па гадзінны стрэлцы. Калі вы дададзіце іх па гадзіннікавай стрэлцы, трохкутнік будзе адбракаваны, і вы яго не ўбачыце.
Няма неабходнасці мець спружыну для кожнага пікселя шырыні. У дэма я выкарыстаў 201 крыніца, які распаўсюджваецца па акна шырынёй 800 пк. Гэта дае роўна 4 пк паміж кожнай спружынай, прычым першая спружына роўная 0, а апошняя - 800 пк. Вы, верагодна, маглі б выкарыстоўваць яшчэ менш спружын і ўсё яшчэ мець гладкую ваду.
Тое, што мы хочам зрабіць, гэта намаляваць тонкія высокія трапецыі, якія распасціраюцца ад ніжняй частцы экрана да паверхні вады і злучаюць спружыны, як паказана на гэтай дыяграме:

Паколькі графічныя карты ня малююць трапецоиды наўпрост, мы павінны намаляваць кожную трапецыю ў выглядзе двух трыкутнікаў. Каб гэта выглядала трохі лепш, мы таксама зробім ваду цямней, калі яна стане глыбей, афарбоўваючы ніжнія вяршыні сінім колерам. GPU аўтаматычна будзе интерполировать колеру паміж вяршынямі.
primitiveBatch.Begin(PrimitiveType.TriangleList); Color midnightBlue = new Color(0, 15, 40) * 0.9f; Color lightBlue = new Color(0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; float bottom = viewport.Height; // stretch the springs' x positions to take up the entire window float scale = viewport.Width / (springs.Length - 1f); // be sure to use float division for (int i = 1; i < springs.Length; i++) { // create the four corners of our triangle. Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p2.X, bottom); Vector2 p4 = new Vector2(p1.X, bottom); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p4, midnightBlue); } primitiveBatch.End();
Вось вынік:
Стварэнне пырскі
Хвалі выглядаюць нядрэнна, але я хацеў бы ўбачыць ўсплёск, калі камень патрапіць у ваду. Эфекты часціц ідэальна падыходзяць для гэтага.
Частковыя эфекты
Эфект часціц выкарыстоўвае вялікая колькасць дробных часціц для атрымання некаторага візуальнага эфекту. Яны часам выкарыстоўваюцца для такіх рэчаў, як дым або іскры. Мы збіраемся выкарыстоўваць часціцы для кропель вады ў пырсках.
Першае, што нам трэба, гэта клас часціц:
class Particle { public Vector2 Position; public Vector2 Velocity; public float Orientation; public Particle(Vector2 position, Vector2 velocity, float orientation) { Position = position; Velocity = velocity; Orientation = orientation; } }
Гэты клас проста захоўвае ўласцівасці, якія можа мець часціца. Затым мы створым спіс часціц.
List<Particle> particles = new List<Particle>();
Кожны кадр мы павінны абнаўляць і маляваць часціцы.
void UpdateParticle(Particle particle) { const float Gravity = 0.3f; particle.Velocity.Y += Gravity; particle.Position += particle.Velocity; particle.Orientation = GetAngle(particle.Velocity); } private float GetAngle(Vector2 vector) { return (float)Math.Atan2(vector.Y, vector.X); } public void Update() { foreach (var particle in particles) UpdateParticle(particle); // delete particles that are off-screen or under water particles = particles.Where(x => x.Position.X >= 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); }
Мы абнаўляем часціцу каб яна падала пад дзеяннем сілы цяжару і ўсталявала арыентацыю часціцы ў адпаведнасці з напрамкам, з якім яна збіраецца. Мы тады пазбавімся ад любых часціц, якія знаходзяцца па-за экранаі або пад вадой, скапіяваўшы ўсе часціцы, якія мы хочам захаваць у новы спіс і прысвоіўшы яго часціцам. Далей мы малюем часціцы.
void DrawParticle(Particle particle) { Vector2 origin = new Vector2(ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw(ParticleImage, particle.Position, null, Color.White, particle.Orientation, origin, 0.6f, 0, 0); } public void Draw() { foreach (var particle in particles) DrawParticle(particle); }
Затым мы малюем часціцы.

Цяпер, калі мы ствараем ўсплёск, мы ствараем кучу часціц.
private void CreateSplashParticles(float xPosition, float speed) { float y = GetHeight(xPosition); if (speed > 60) { for (int i = 0; i < speed / 8; i++) { Vector2 pos = new Vector2(xPosition, y) + GetRandomVector2(40); Vector2 vel = FromPolar(MathHelper.ToRadians(GetRandomFloat(-150, -30)), GetRandomFloat(0, 0.5f * (float)Math.Sqrt(speed))); particles.Add(new Particle(pos, velocity, 0)); } } }
Вы можаце выклікаць гэты метад з метаду Splash()
, які мы выкарыстоўваем для стварэння хваль. Хуткасць параметру - гэта хуткасць, з якой камень трапляе ў ваду. Мы будзем рабіць вялікія воплескі, калі скала будзе рухацца хутчэй.
GetRandomVector2(40)
вяртае вектар са выпадковым кірункам і выпадковую даўжыню паміж 0 і 40. Мы хочам дадаць трохі выпадковасці да пазіцый, каб часціцы не ўсе адлюстроўваліся ў адным пункце. FromPolar()
вяртае Vector2
з зададзеным кірункам і даўжынёй.
Вось вынік:
Выкарыстанне метабаллов ў якасці часціц
Нашы пырскі выглядаюць даволі прыстойна, а некаторыя выдатныя гульні, такія як World of Goo, маюць воплескі эфектаў часціц, якія выглядаюць так жа, як нашы. Аднак я пакажу вам тэхніку, каб пырскі выглядалі больш вадкімі. У тэхніцы выкарыстоўваюцца метабалы, арганічныя бурбалкі, пра якія я ўжо пісаў падручнік. Калі вас цікавяць падрабязнасці аб метабаллах і як яны працуюць, прачытайце гэты падручнік. Калі вы проста хочаце ведаць, як прымяняць іх да нашых усплёскаў, працягвайце чытаць.
Метабаллы выглядаюць падобнымі на вадкасць, так як яны зліваюцца адзін з адным, што робіць іх прыдатнымі для нашых вадкіх пырскаў. Каб зрабіць метабаллы, нам трэба будзе дадаць новыя зменныя класа:
RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;
Што мы ініцыялізуем так:
var view = GraphicsDevice.Viewport; metaballTarget = new RenderTarget2D(GraphicsDevice, view.Width, view.Height); alphaTest = new AlphaTestEffect(GraphicsDevice); alphaTest.ReferenceAlpha = 175; alphaTest.Projection = Matrix.CreateTranslation(-0.5f, -0.5f, 0) * Matrix.CreateOrthographicOffCenter(0, view.Width, view.Height, 0, 0, 1);
Затым мы малюем метабаллы:
GraphicsDevice.SetRenderTarget(metaballTarget); GraphicsDevice.Clear(Color.Transparent); Color lightBlue = new Color(0.2f, 0.5f, 1f); spriteBatch.Begin(0, BlendState.Additive); foreach (var particle in particles) { Vector2 origin = new Vector2(ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw(ParticleImage, particle.Position, null, lightBlue, particle.Orientation, origin, 2f, 0, 0); } spriteBatch.End(); GraphicsDevice.SetRenderTarget(null); device.Clear(Color.CornflowerBlue); spriteBatch.Begin(0, null, null, null, null, alphaTest); spriteBatch.Draw(metaballTarget, Vector2.Zero, Color.White); spriteBatch.End(); // draw waves and other things
Эфект метабалла залежыць ад наяўнасці тэкстуры часціц, якая знікае, калі вы рухаецеся далей ад цэнтра. Вось што я выкарыстаў, устанавіў на чорным фоне, каб зрабіць яго бачным:

Вось як гэта выглядае:
Зараз кроплі вады зліваюцца, калі яны блізкія. Аднак яны не зліваюцца з паверхняй вады. Мы можам выправіць гэта, дадаўшы градыент да паверхні вады, што робіць яго паступова знікаць і ператварае яго ў нашу мэта рэндэрынгу метабалла.
Дадайце наступны код у метад вышэй да лініі GraphicsDevice.SetRendertarget (null)
:
primitiveBatch.Begin(PrimitiveType.TriangleList); const float thickness = 20; float scale = GraphicsDevice.Viewport.Width / (springs.Length - 1f); for (int i = 1; i < springs.Length; i++) { Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p1.X, p1.Y - thickness); Vector2 p4 = new Vector2(p2.X, p2.Y - thickness); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p4, Color.Transparent); primitiveBatch.AddVertex(p2, lightBlue); } primitiveBatch.End();
Цяпер часціцы будуць злівацца з паверхняй вады
Даданне эфекту скосу
Часціцы вады выглядаюць трохі плоскімі, і было б нядрэнна даць ім некаторы зацяненне. У ідэале вы б зрабілі гэта ў шэйдараў. Аднак, дзеля таго, каб гэты падручнік быў просты, мы збіраемся выкарыстоўваць хуткі і просты трук: мы проста збіраемся маляваць часціцы тры разы з рознымі адценнямі і зрушэннямі, як паказана на дыяграме ніжэй.



Для гэтага мы хочам захапіць часціцы метабалла ў новай мэты рэндэрынгу. Затым мы намалюем гэтую мэту рэндэрынгу адзін раз для кожнага адцення.
Па-першае, абвясціце новы RenderTarget2D
гэтак жа, як і для метабаков:
particlesTarget = new RenderTarget2D(GraphicsDevice, view.Width, view.Height);
Затым, замест таго, каб маляваць metaballsTarget
непасрэдна ў буферны буфер, мы хочам намаляваць яго на particleTarget
. Для гэтага перайдзіце да метаду, дзе мы малюем метабаллы і проста мяняем гэтыя радкі:
GraphicsDevice.SetRenderTarget(null); device.Clear(Color.CornflowerBlue);
... каб:
GraphicsDevice.SetRenderTarget(particlesTarget); device.Clear(Color.Transparent);
Затым выкарыстоўвайце наступны код, каб маляваць часціцы тры разы з рознымі адценнямі і зрушэннямі:
Color lightBlue = new Color(0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget(null); device.Clear(Color.CornflowerBlue); spriteBatch.Begin(); spriteBatch.Draw(particlesTarget, -Vector2.One, new Color(0.8f, 0.8f, 1f)); spriteBatch.Draw(particlesTarget, Vector2.One, new Color(0f, 0f, 0.2f)); spriteBatch.Draw(particlesTarget, Vector2.Zero, lightBlue); spriteBatch.End(); // draw waves and other stuff
Выснова
Гэта для базавай 2D вады. Для дэманстрацыі я дадаў камень, які вы можаце скінуць у ваду. Я малюю ваду з некаторай празрыстасцю на вяршыні скалы, каб яна выглядала так, быццам яна пад вадой, і запавольвае яе, калі яна пад вадой з-за супраціву вады.
Каб зрабіць дэманстрацыйны прыклад трохі прыемней, я пайшоў на opengameart.org і знайшоў малюнак для скалы і фону неба. Вы можаце знайсці скалу і неба па адрасе http://opengameart.org/content/rocks і opengameart.org/content/sky-backdrop адпаведна.