В данной программе будет представлен пример, как работать с объёмными фигурами в
OpenGL на языке Python. В итоге будет представлен куб в 3-ёх мерном виде.
Среда разработки: PyCharm Community Edition 2020.1.2
Для начала работы потребуется наличие следующих инструментов или пакетов: Python, PyOpenGL, PyGame.
Как установить Python можно посмотреть на официальном сайте, ссылка на него: https://www.python.org/
Далее рассмотрим установку пакетов на Python.
Для того, чтобы загрузить PyOpenGL и PyGame, воспользуемся PyPi. В терминале IDE вводим команды:
pip install pygame pip install PyOpenGL
Загрузив все это, пишем следующий код:
import pygame import OpenGL
Если эти выражения не вызовут ошибок, значит вы готовы к дальнейшей работе. Если возникнут ошибки, значит при установке пакетов произошла ошибка, либо текущие версии пакетов несовместимы с Вашей версией Python. Более детальную информацию можно узнать в документации к используемым пакетам Python’a: PyGame и PyOpenGL.
ВАША ПЕРВАЯ 3Д МОДЕЛЬ В BLENDER
Отлично, теперь перейдем непосредственно к коду! Если у вас еще сохранился код импорта, который мы только что запускали, сотрите его: мы начнем полностью с чистого листа.
Для начала произведем импорт необходимых библиотек:
import pygame from pygame.locals import * from OpenGL.GL import * from OpenGL.GLU import *
Мы импортируем все из PyGame, а затем все из PyGame.locals. Это вполне стандартный код для PyGame.Затем мы импортируем OpenGL.GL и OpenGL.GLU. OpenGL.GL содержит в себе самые обычные функции библиотеки OpenGL, а вот OpenGL.GLU — более сложных объектов.
Изначально создадим функцию main() для удобства:
def main():
Теперь начнем описывать нашу функцию main().
Давайте же создадим описание наших точек (вершин):
vertices= ( (1.0, -1.0, -1.0), (1.0, 1.0, -1.0), (-1.0, 1.0, -1.0), (-1.0, -1.0, -1.0), (1.0, — 1.0, 1.0), (1.0, 1.0, 1.0), (-1.0, -1.0, 1.0), (-1.0, 1.0, 1.0) )
Здесь мы определили координаты (x, y, z) каждой нашей вершины. Думаю, лучше всего представить это в относительных единицах.
Далее нам надо задать ребра:
edges = ( (0,1), (0,3), (0,4), (2,1), (2,3), (2,7), (6,3), (6,4), (6,7), (5,1), (5,4), (5,7) )
Каждое ребро представлено кортежем, состоящим из двух чисел. Эти числа соответствуют номерам вершин, а ребро их соединяет. Как принято в Python, да и во многих других языках программирования, нумерация начинается с 0. Соответственно, 0 обозначает вершину (1, -1, -1), и так далее.
Теперь, когда у нас все это есть, давайте поработаем над необходимым кодом для работы с OpenGL, чтобы фактически создать сам куб (описывать функцию Cube() мы будем вне функции main()):
def Cube(vertices, edges): glBegin(GL_LINES) for edge in edges: for vertex in edge: glVertex3fv(vertices[vertex]) glEnd()
Как обычно, мы начинаем с задания функции. Поскольку это просто функция, которая будет содержать код OpenGL, мы начинаем этот код со следующего выражения: glBegin (GL_LINES). Это уведомляет OpenGL сначала о том, что мы собираемся бросить в нее какой-то код, а затем — о том, как надо обрабатывать этот код (это указывает аргумент GL_LINES). В данном случае этот код будет рассматриваться как код для рисования линий.
Как начать заниматься 3D и какую программу выбрать
Далее, мы итерируем по всем нашим ребрам (список edges), а затем каждой вершине в ребре (их там две) мы ставим в соответствие вершину из нашего списка вершин vertices (при помощи функции glVertex3fv).
glVertex3fv((1, -1, -1)) glVertex3fv((1, 1, -1))
И так далее. OpenGL, зная, что мы хотим рисовать здесь линии, проведет их между этими точками.
После прохождения всех ребер работа функции заканчивается и мы вызываем glEnd(), чтобы сообщить об этом OpenGL. Подобные открывающие и закрывающие команды используются в OpenGL постоянно.
Это все, что касается нашей функции. Она создает куб, но теперь мы хотим его отобразить и указать его перспективу в нашем окружении, для этого возвращаемся в функцию main() и пишем следующее:
pygame.init() display = (800,600) pygame.display.set_mode(display, DOUBLEBUF|OPENGL)
Это тоже очень типичный для Pygame код. Единственное существенное отличие здесь в том, что после параметра display в функции pygame.display.set_mode мы добавляем еще один параметр. На самом деле это константы, уведомляющие PyGame о том, что мы будем использовать код OpenGL. Константа DOUBLEBUF расшифровывается как двойной буфер.
Она обозначает тип буфферизации, в котором есть два буфера для соответствия кадровой частоте монитора. Обратите внимание, что для разделения констант используется символ «|». Мы еще столкнемся с ним в дальнейшем.
Идем дальше. В теле главной функции находится следующий код:
gluPerspective(45, (display[0]/display[1]), 0.1, 50.0)
Функция gluPerspective определяет перспективу, о чем, впрочем, несложно догадаться из ее названия. Ее первый параметр определяет угол поля зрения и выражается в градусах. Второй параметр — это соотношение сторон дисплея, ширина, деленная на высоту. Следующие два параметра — znear и zfar, которые представляют собой ближнюю и дальнюю плоскости отсечения.
Идем дальше. У нас есть следующая функция:
glTranslatef(0.0, -1.0, -10) glRotatef(20, 10, 0, 0)
Функция glTranslatef, цитируя дословно, «умножает текущую матрицу на матрицу перехода»
Теперь напишем наш типичный цикл для отслеживания событий в PyGame:
while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit()
Это простой цикл отслеживания событий в PyGame, который определяет возможность выхода. Иными словами, он отслеживает нажатие клавиши «x». Далее, под оператором while продолжаем наш код:
glRotatef(1, 0, 1, 0) glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) Cube() pygame.display.flip() pygame.time.wait(10)
Функция glRotatef умножает текущую матрицу на матрицу вращения. Ее параметрами являются угол вращения и координаты x, y, и z.
Затем у нас есть функция glClear, работающая, как любая другая функция очистки. Мы указываем в ее параметрах пару констант, которые сообщают OpenGL, что именно мы очищаем.
Как только мы очистим «экран», мы опять вызовем нашу функцию Cube ().
После этого мы вызываем функцию pygame.display.flip (), которая обновляет наш экран.
И наконец, мы вставляем небольшую задержку при помощи функции pygame.time.wait (10).
И в завершении в самой программе просто вызываем функцию main().
import pygame from pygame.locals import * from OpenGL.GL import * from OpenGL.GLU import * def Cube(vertices, edges): glBegin(GL_LINES) for edge in edges: for vertex in edge: glVertex3fv(vertices[vertex]) glEnd() def main(): vertices = ( (1.0, -1.0, -1.0), (1.0, 1.0, -1.0), (-1.0, 1.0, -1.0), (-1.0, -1.0, -1.0), (1.0, — 1.0, 1.0), (1.0, 1.0, 1.0), (-1.0, -1.0, 1.0), (-1.0, 1.0, 1.0) ) edges = ( (0, 1), (0, 3), (0, 4), (2, 1), (2, 3), (2, 7), (6, 3), (6, 4), (6, 7), (5, 1), (5, 4), (5, 7) ) pygame.init() display = (800, 600) pygame.display.set_mode(display, DOUBLEBUF|OPENGL) gluPerspective(45, (display[0]/display[1]), 0.1, 50.0) glTranslatef(0.0, -1.0, -10) glRotatef(20, 10, 0, 0) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() glRotatef(1, 0, 1, 0) glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) Cube(vertices, edges) pygame.display.flip() pygame.time.wait(10) main()
main.rar | 573 байта |
Источник: grafika.me
Написание 3D Soft Engine 2/6
Теперь, когда мы построили ядро нашего 3D-движка по аналогии с нашим предыдущим уроком № 1, теперь мы будем работать над улучшением рендеринга. Следующим шагом будет соединение точек, чтобы нарисовать несколько линий, чтобы визуализировать то, что вы, вероятно, знаете как визуализацию «каркаса». В сегодняшнем уроке вы узнаете, как рисовать линии, что такое лицо и насколько крут алгоритм Брезенхема для рисования треугольников.
Благодаря этому, в конце концов, вы будете знать, как написать что-то столь же крутое:
Да! Наш 3D вращающийся куб начинает реально жить на наших экранах!
Первый базовый алгоритм для рисования линии между двумя точками
Начнем с написания простого алгоритма. К провести линию между двумя вершинамимы будем использовать следующую логику:
— если расстояние между 2 точками (точка0 и точка1) меньше 2 пикселей, то делать нечего
— в противном случае мы находим средняя точка между обеими точками (координаты point0 + (координаты point1 – координаты point0) / 2)
— мы рисуем эту точку на экране
— мы запускаем это алгоритм рекурсивно между точкой0 и средней точкой и между средней точкой и точкой1
Вот код для этого:
public void DrawLine(Vector2 point0, Vector2 point1) var dist = (point1 — point0).Length(); // If the distance between the 2 points is less than 2 pixels // We’re exiting if (dist
public drawLine(point0: BABYLON.Vector2, point1: BABYLON.Vector2): void < var dist = point1.subtract(point0).length(); // If the distance between the 2 points is less than 2 pixels // We’re exiting if (dist
Device.prototype.drawLine = function (point0, point1) < var dist = point1.subtract(point0).length(); // If the distance between the 2 points is less than 2 pixels // We’re exiting if(dist
You need to update the rendering loop to use this new piece of code:
for (var i = 0; i
for (var i = 0; i
for (var i = 0; i
And you should now obtain something like that:
I know this looks weird but this was the expected behavior. It should help you starting to understand what you need to do to display a 3D mesh. But to have a better rendering, we need to discover a new concept.
Displaying faces with triangles
Now that we know how to draw lines, we need a better way to render the mesh with them. Thesimplest geometric 2D shape is a triangle. The idea in 3D is then to draw all our meshes by using those triangles. We then need to split each side of our cube into 2 triangles. We’re going to do this “manually” but we’ll see in the next tutorial that 3D modelers are doing this step automatically for us now.
To draw triangles, you need to have 3 points/vertices. A face is then simply a structure containing 3 values which are indexes pointing to the proper vertices array of the mesh to be rendered.
To be understand this concept, let’s take our previous figure with a Cube displayed by Blender:
We have 4 vertices displayed on this figure with the following indices: 0, 1, 2, 3. To draw the upper side of the cube, we need to draw 2 triangles. The first one, Face 0, will be drawn with 3 lines from vertex 0(-1, 1, 1) to vertex 1 (1, 1, 1), from vertex 1 (1, 1, 1) to vertex 2 (-1, –1, 1) and finally from vertex 2 (-1, –1, 1) to vertex 0 (-1, 1, 1). The second triangle, Face 1, will be drawn with the lines from vertex 1 to vertex 2, vertex 2 to vertex 3 and vertex 3 to vertex 1.
The equivalent code would be something like that:
var mesh = new SoftEngine.Mesh(«Square», 4, 2); eshes.Add(mesh); esh.Vertices[0] = new Vector3(-1, 1, 1); esh.Vertices[1] = new Vector3(1, 1, 1); esh.Vertices[2] = new Vector3(-1, -1, 1); esh.Vertices[3] = new Vector3(1, -1, 1); mesh.Faces[0] = new Face < A = 0, B = 1, C = 2 >; esh.Faces[1] = new Face < A = 1, B = 2, C = 3 >;
Если вы хотите нарисовать весь куб, вам нужно найти 10 оставшихся граней, как у нас 12 лиц для рисования 6 сторон нашего куба.
Давайте теперь определим код для Лицо объект. Это очень простой объект, так как это просто набор из 3 индексов. Вот код Face и нового определения Mesh, которые теперь также используют его:
namespace SoftEngine public struct Face < public int A; public int B; public int C; >public class Mesh < public string Name < get; set; >public Vector3[] Vertices < get; private set; >public Face[] Faces < get; set; >public Vector3 Position < get; set; >public Vector3 Rotation < get; set; >public Mesh(string name, int verticesCount, int facesCount) < Vertices = new Vector3[verticesCount]; Faces = new Face[facesCount]; Name = name; >>
/// module SoftEngine < export interface Face < A: number; B: number; C: number; >export class Mesh < Position: BABYLON.Vector3; Rotation: BABYLON.Vector3; Vertices: BABYLON.Vector3[]; Faces: Face[]; constructor(public name: string, verticesCount: number, facesCount: number) < this.Vertices = new Array(verticesCount); this.Faces = new Array(facesCount); this.Rotation = new BABYLON.Vector3(0, 0, 0); this.Position = new BABYLON.Vector3(0, 0, 0); >>
var SoftEngine; function (SoftEngine) < var Mesh = (function () < function Mesh(name, verticesCount, facesCount) < this.name = name; this.Vertices = new Array(verticesCount); this.Faces = new Array(facesCount); this.Rotation = new BABYLONTS.Vector3(0, 0, 0); this.Position = new BABYLONTS.Vector3(0, 0, 0); >return Mesh; >)(); SoftEngine.Mesh = Mesh; )(SoftEngine || (SoftEngine = <>));
Теперь нам нужно обновить наш Оказывать() функция/метод нашего Устройство объект для перебора всех определенных граней и рисования связанных треугольников.
foreach (var face in mesh.Faces) var vertexA = mesh.Vertices[face.A]; var vertexB = mesh.Vertices[face.B]; var vertexC = mesh.Vertices[face.C]; var pixelA = Project(vertexA, transformMatrix); var pixelB = Project(vertexB, transformMatrix); var pixelC = Project(vertexC, transformMatrix); DrawLine(pixelA, pixelB); DrawLine(pixelB, pixelC); DrawLine(pixelC, pixelA);
for (var indexFaces = 0; indexFaces
for (var indexFaces = 0; indexFaces
We finally need to declare the mesh associated with our Cube properly with its 12 faces to make this new code working as expected.
Here is the new declaration:
var mesh = new SoftEngine.Mesh(«Cube», 8, 12); eshes.Add(mesh); esh.Vertices[0] = new Vector3(-1, 1, 1); esh.Vertices[1] = new Vector3(1, 1, 1); esh.Vertices[2] = new Vector3(-1, -1, 1); esh.Vertices[3] = new Vector3(1, -1, 1); esh.Vertices[4] = new Vector3(-1, 1, -1); esh.Vertices[5] = new Vector3(1, 1, -1); esh.Vertices[6] = new Vector3(1, -1, -1); esh.Vertices[7] = new Vector3(-1, -1, -1); mesh.Faces[0] = new Face < A = 0, B = 1, C = 2 >; esh.Faces[1] = new Face < A = 1, B = 2, C = 3 >; esh.Faces[2] = new Face < A = 1, B = 3, C = 6 >; esh.Faces[3] = new Face < A = 1, B = 5, C = 6 >; esh.Faces[4] = new Face < A = 0, B = 1, C = 4 >; esh.Faces[5] = new Face < A = 1, B = 4, C = 5 >; mesh.Faces[6] = new Face < A = 2, B = 3, C = 7 >; esh.Faces[7] = new Face < A = 3, B = 6, C = 7 >; esh.Faces[8] = new Face < A = 0, B = 2, C = 7 >; esh.Faces[9] = new Face < A = 0, B = 4, C = 7 >; esh.Faces[10] = new Face < A = 4, B = 5, C = 6 >; esh.Faces[11] = new Face < A = 4, B = 6, C = 7 >;
var mesh = new SoftEngine.Mesh(«Cube», 8, 12); eshes.push(mesh); esh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1); esh.Vertices[1] = new BABYLON.Vector3(1, 1, 1); esh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1); esh.Vertices[3] = new BABYLON.Vector3(1, -1, 1); esh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1); esh.Vertices[5] = new BABYLON.Vector3(1, 1, -1); esh.Vertices[6] = new BABYLON.Vector3(1, -1, -1); esh.Vertices[7] = new BABYLON.Vector3(-1, -1, -1); mesh.Faces[0] = < A:0, B:1, C:2 >; esh.Faces[1] = < A:1, B:2, C:3 >; esh.Faces[2] = < A:1, B:3, C:6 >; esh.Faces[3] = < A:1, B:5, C:6 >; esh.Faces[4] = < A:0, B:1, C:4 >; esh.Faces[5] = < A:1, B:4, C:5 >; mesh.Faces[6] = < A:2, B:3, C:7 >; esh.Faces[7] = < A:3, B:6, C:7 >; esh.Faces[8] = < A:0, B:2, C:7 >; esh.Faces[9] = < A:0, B:4, C:7 >; esh.Faces[10] = < A:4, B:5, C:6 >; esh.Faces[11] = < A:4, B:6, C:7 >;
var mesh = new SoftEngine.Mesh(«Cube», 8, 12); eshes.push(mesh); esh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1); esh.Vertices[1] = new BABYLON.Vector3(1, 1, 1); esh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1); esh.Vertices[3] = new BABYLON.Vector3(1, -1, 1); esh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1); esh.Vertices[5] = new BABYLON.Vector3(1, 1, -1); esh.Vertices[6] = new BABYLON.Vector3(1, -1, -1); esh.Vertices[7] = new BABYLON.Vector3(-1, -1, -1); mesh.Faces[0] = < A:0, B:1, C:2 >; esh.Faces[1] = < A:1, B:2, C:3 >; esh.Faces[2] = < A:1, B:3, C:6 >; esh.Faces[3] = < A:1, B:5, C:6 >; esh.Faces[4] = < A:0, B:1, C:4 >; esh.Faces[5] = < A:1, B:4, C:5 >; mesh.Faces[6] = < A:2, B:3, C:7 >; esh.Faces[7] = < A:3, B:6, C:7 >; esh.Faces[8] = < A:0, B:2, C:7 >; esh.Faces[9] = < A:0, B:4, C:7 >; esh.Faces[10] = < A:4, B:5, C:6 >; esh.Faces[11] = < A:4, B:6, C:7 >;
Теперь у вас должен получиться этот красивый вращающийся куб:
Улучшение алгоритма рисования линий с помощью Bresenham
Существует оптимизированный способ рисования наших линий с использованием алгоритма линий Брезенхэма. Это быстрее и четче, чем наша текущая простая рекурсивная версия. История этого алгоритма увлекательна. Пожалуйста, прочтите определение этого алгоритма в Википедии, чтобы узнать, как Брезенхэм его построил и по каким причинам.
Вот версии этого алгоритма на C#, TypeScript и JavaScript:
public void DrawBline(Vector2 point0, Vector2 point1) int x0 = (int)point0.X; int y0 = (int)point0.Y; int x1 = (int)point1.X; int y1 = (int)point1.Y; var dx = Math.Abs(x1 — x0); var dy = Math.Abs(y1 — y0); var sx = (x0 -dy) < err -= dy; x0 += sx; >if (e2
public drawBline(point0: BABYLON.Vector2, point1: BABYLON.Vector2): void < var x0 = point0.x >> 0; var y0 = point0.y >> 0; var x1 = point1.x >> 0; var y1 = point1.y >> 0; var dx = Math.abs(x1 — x0); var dy = Math.abs(y1 — y0); var sx = (x0 -dy) < err -= dy; x0 += sx; >if (e2
Написание 3D Soft Engine 3/6 В предыдущем уроке мы научились рисовать линии и треугольники. Мы действительно начали видеть 3D-сторону наших…
13 бесплатных тем WordPress, выпущенных в июне Теперь, когда июнь закончился, пришло время взглянуть на некоторые из лучших бесплатных тем WordPress, выпущенных…
Запрещено проникновение! Восемь основных трюков с .htaccess для WordPress Файл .htaccess — это мощный инструмент настройки веб-серверов или пакетов веб-хостинга, который всегда следует использовать…
Как создать плагин для удаленного тестирования JavaScript Во время выступления на недавней конференции //BUILD 2015 наша команда Microsoft выпустила Vorlon.js, инструмент для…
Использование замыкающего пространства для создания реальных закрытых членов Недавно я разработал Angular Cloud Data Connector, который позволяет разработчикам Angular использовать облачные данные, в…
Источник: nikitavolkov.com
WebGL как современное искусство
Вы наверняка встречали на просторах интернета веб-сайты с интерактивной 3D-графикой, с возможностью взаимодействия с ней, когда можно покрутить какой-то предмет, рассмотреть его со всех сторон, кликнуть мышкой. Или просто завораживающую анимацию, меняющуюся при скролле страницы и очень похожую на интерактивное видео.
Многие такие сайты созданы с помощью технологии WebGL. Читайте дальше, чтобы узнать больше об этом инструменте, его возможностях, преимуществах и привлекательности как для пользователей, так и для бизнеса.
Что такое WebGL?
WebGL (Web Graphics Library, или библиотека веб-графики) — это программный интерфейс для создания интерактивной веб-графики. Простыми словами, WebGL работает как средство растеризации. Оно отображает точки, линии и треугольники на основе написанного кода в виде привлекательного 2D или 3D-изображения. Это позволяет нам видеть трехмерный контент прямо в браузере.
Все началось с момента появления стандарта HTML5 в 2014 году, который был призван улучшить уровень поддержки мультимедиа-технологий. HTML5, помимо прочего, привнес такие элементы, как video, audio и canvas (в переводе с английского «холст»). С помощью canvas появилась возможность рисовать различные элементы на странице: текст, линии, фигуры, изображения.
WebGL, подобно искусному художнику, легким взмахом кисти мгновенно переносит 3D-модель на холст. В нашем случае холстом является элемент HTML5 — Canvas. В роли кистей и красок выступают языки программирования JavaScript и GLSL.
Как это работает?
Чтобы понять основы WebGL, полезно знать базовую теорию 3D-графики и процесс рендеринга. В 3D-сцене каждая точка представляет собой вершину, идентифицируемую своими координатами x, y, z. Затем вершины соединяются, чтобы сформировать группу треугольных форм (примитивов). Далее применяется источник света для создания теней и глубины изображения. Затем примитивы преобразуются для создания трехмерной векторной графики в виде проекции двухмерного пикселя, который при помощи иллюзии заставляет мозг видеть трехмерный объект на двухмерном экране компьютера.
В коде WebGL есть два типа функций:
фрагментные шейдеры.
вершинные шейдеры,
Они применяются, чтобы сообщить компьютеру, как рисовать пиксели на экране. Хотя основной программный код написан на JavaScript, шейдеры используют язык шейдеров GL.
Вершинный шейдер вычисляет координаты вершин, а фрагментный шейдер отвечает за вычисление цветов пикселей. Процесс затенения требует от компьютера множество вычислений для плавного рендеринга изображений, что часто приводит к слишком большой нагрузке на центральный процессор. По этой причине WebGL использует графические процессоры (GPU). В отличие от центрального процессора устройства, имеющего несколько ядер, которые заточены на последовательную обработку, графический процессор (GPU) имеет тысячи меньших ядер, выполняющих более простые операции, но при этом параллельно. GPU может одновременно выполнять миллионы вычислений и фокусируется на одной единственной цели — молниеносно отрисовать графику.
В итоге, WebGL генерирует изображение из 3D-модели с помощью графического процессора, производя так называемый аппаратный рендеринг. Это дает много свободы для творчества и создания графики на сайте.
Инструменты. Почему Three.js?
К счастью, сегодня нет необходимости писать программы с нуля, чтобы начать добавлять 3D-графику на свой сайт. Вы можете использовать такие ресурсы, как three. js, Pixi JS и другие подобные библиотеки для быстрого проектирования 3D-приложений без глубоких знаний о WebGL.
Большинство разработчиков выбирают инструмент three.js, и этому есть несколько причин:
выполняет всю низкоуровневую работу с WebGL, не отвлекая разработчика от самого важного. Создание чего-либо стоящего на чистом WebGL без привлечения инструментов, подобных three. js, потребует немало кода;
предоставляет разработчику понятный интерфейс для работы с WebGL, позволяя вести разработку 3D-графики при помощи определенных сущностей — сцены, источника света, теней, материалов и текстур;
Источник: www.uplab.ru