Всем привет! Это разработческий канал от сотрудников Red Collar.
Каждую неделю один из сотрудников компании рассказывает о своих задачах, сложностях, решениях, делится полезными ссылками и мыслями на тему разработки.
Стремимся применять современные языки и новые подходы в разработке, ценим качественную работу, любим сложные задачи и открыты к критике, если вы тоже — мы на одной волне. :)
В компании занимаемся двумя направлениями:
- Разработкой сложных продуктов, сервисов для сотен тысяч пользователей для ведущих финтех-, телеком-, IT-, HoReCa- и логистических компаний.
- Созданием эстетичных корпоративных и промосайтов, которые становятся лучшими в своей сфере и берут самые сложные международные награды.
По тегам можно найти темы:
#rdclr_frontend — #Vanilla_JS #JavaScript #React #WebGL
#rdclr_backend — #Java #Python #PHP #NN
#rdclr_DevOps
#rdclr_QA
#product (мысли, применимые к сервисно-продуктовым историям)
#aesthetic (про эстетичность интерфейсов) #teamlead — про роль, практики и рост в тим-лида
#optimization (про оптимизацию кода для скорости и плавности работы проекта) и #library (набор инструментов для упрощения жизни разработчика)
#read (полезные ссылки для изучения нового материала)
#meme (на посмеяться) и #news (новости)
Чтобы совместить два тега — введите оба в поисковую строку канала.
Каждую неделю один из сотрудников компании рассказывает о своих задачах, сложностях, решениях, делится полезными ссылками и мыслями на тему разработки.
Стремимся применять современные языки и новые подходы в разработке, ценим качественную работу, любим сложные задачи и открыты к критике, если вы тоже — мы на одной волне. :)
В компании занимаемся двумя направлениями:
- Разработкой сложных продуктов, сервисов для сотен тысяч пользователей для ведущих финтех-, телеком-, IT-, HoReCa- и логистических компаний.
- Созданием эстетичных корпоративных и промосайтов, которые становятся лучшими в своей сфере и берут самые сложные международные награды.
По тегам можно найти темы:
#rdclr_frontend — #Vanilla_JS #JavaScript #React #WebGL
#rdclr_backend — #Java #Python #PHP #NN
#rdclr_DevOps
#rdclr_QA
#product (мысли, применимые к сервисно-продуктовым историям)
#aesthetic (про эстетичность интерфейсов) #teamlead — про роль, практики и рост в тим-лида
#optimization (про оптимизацию кода для скорости и плавности работы проекта) и #library (набор инструментов для упрощения жизни разработчика)
#read (полезные ссылки для изучения нового материала)
#meme (на посмеяться) и #news (новости)
Чтобы совместить два тега — введите оба в поисковую строку канала.
Проекция в WebGL, или «алло а камера где дядя»
Когда я только начинал заниматься графикой (а точнее, на тот момент еще геймдевом), совсем зеленый и не нюхавший так сказать полигонов, я взялся за движок Blitz3D. Он был настолько прост, что даже для неподготовленного человека взять и собрать сцену было проще простого. Взял пространство, поставил туда камеру, повесил кубик и уже идешь хвалиться родителям, что ты Senior Graphics Developer. 😎 И вот ты приходишь в OpenGL (да, именно десктопный, про WebGL будет чуть позже) и думаешь «ну я уже все знаю, сейчас все быстренько накину, и все как обычно отрендерится!». Но начинающего разработчика графики ждало крупное разочарование.
На самом деле, в низкоуровневой графике вообще нет такого понятия, как камера. Все, что на самом деле умеет WebGL — это растрировать полигоны, переводя их координаты в экранное пространство.
💅🏻 Так как же это происходит, и что для этого нужно сделать?
WebGL представляет твой экран, как трехмерную координатную систему, где X смотрит вправо, Y вверх, а Z-координата направлена из монитора прямо на тебя (одна из самых нелогичных вещей для неподготовленного человека, так как во всех игровых движках Z обычно смотрит вглубь экрана). Если представить это все в виде куба, то его размеры будут от (-1, -1, -1) до (1, 1, 1), когда (0, 0, 0) будет центром экрана, а ребра этого куба будут краями экрана. И для того, чтобы наша моделька отобразилась, нужно всю ее геометрию преобразовать каким-то неведомым образом так, чтобы она вписывалась в этот куб, иначе мы рискуем отрисовать ее за пределами экрана. Я нарочно не упоминаю про Z-координату, так как не очень просто будет с наскока объяснить, что она из себя здесь представляет, но не волнуйтесь, чуть позже мы к ней вернемся.
🪢 Так как же нам обработать модель так, чтобы она оказалась в этом кубе?
На помощь нам придут матрицы (ну, которые математические, давайте без приколов).
Матрицы в WebGL представляют собой массив из 16 дробных чисел, в виде 4х4, в которых содержится информация о повороте, сдвиге и размере. И вот уже читатель начинает догадываться: «то есть нам надо построить какую-то специфическую матрицу, и уже на нее помножить все точки нашей модельки». И это абсолютно верно, с одной маленькой ремаркой — матриц у нас будет не одна, а целых три: матрица проекции, матрица отображения и матрица объекта. Давайте быстренько их и разберем.
#rdclr_frontend #WebGL
Когда я только начинал заниматься графикой (а точнее, на тот момент еще геймдевом), совсем зеленый и не нюхавший так сказать полигонов, я взялся за движок Blitz3D. Он был настолько прост, что даже для неподготовленного человека взять и собрать сцену было проще простого. Взял пространство, поставил туда камеру, повесил кубик и уже идешь хвалиться родителям, что ты Senior Graphics Developer. 😎 И вот ты приходишь в OpenGL (да, именно десктопный, про WebGL будет чуть позже) и думаешь «ну я уже все знаю, сейчас все быстренько накину, и все как обычно отрендерится!». Но начинающего разработчика графики ждало крупное разочарование.
На самом деле, в низкоуровневой графике вообще нет такого понятия, как камера. Все, что на самом деле умеет WebGL — это растрировать полигоны, переводя их координаты в экранное пространство.
💅🏻 Так как же это происходит, и что для этого нужно сделать?
WebGL представляет твой экран, как трехмерную координатную систему, где X смотрит вправо, Y вверх, а Z-координата направлена из монитора прямо на тебя (одна из самых нелогичных вещей для неподготовленного человека, так как во всех игровых движках Z обычно смотрит вглубь экрана). Если представить это все в виде куба, то его размеры будут от (-1, -1, -1) до (1, 1, 1), когда (0, 0, 0) будет центром экрана, а ребра этого куба будут краями экрана. И для того, чтобы наша моделька отобразилась, нужно всю ее геометрию преобразовать каким-то неведомым образом так, чтобы она вписывалась в этот куб, иначе мы рискуем отрисовать ее за пределами экрана. Я нарочно не упоминаю про Z-координату, так как не очень просто будет с наскока объяснить, что она из себя здесь представляет, но не волнуйтесь, чуть позже мы к ней вернемся.
🪢 Так как же нам обработать модель так, чтобы она оказалась в этом кубе?
На помощь нам придут матрицы (ну, которые математические, давайте без приколов).
Матрицы в WebGL представляют собой массив из 16 дробных чисел, в виде 4х4, в которых содержится информация о повороте, сдвиге и размере. И вот уже читатель начинает догадываться: «то есть нам надо построить какую-то специфическую матрицу, и уже на нее помножить все точки нашей модельки». И это абсолютно верно, с одной маленькой ремаркой — матриц у нас будет не одна, а целых три: матрица проекции, матрица отображения и матрица объекта. Давайте быстренько их и разберем.
#rdclr_frontend #WebGL
👍6❤3
Матрица проекции, матрица отображения и матрица объекта в WebGL
🪡 Матрица объекта — это самая простая в объяснении из всех трех. Допустим, у нас есть стул (ну, в реалиях WebGL это будет просто пачка вершин и треугольников, но давайте оперировать человеческим языком). И вот у нас появляется задача: нужно этот стул переставить в пространстве. Что мы будем делать? Правильно, запишем в матрицу объекта значения, на которые нам нужно подвинуть наш стул. Хотите повернуть его на 90 градусов? Правильно, запишем в матрицу данные о размерах. Стул слишком большой? Я думаю, можно не продолжать.
🧵 Матрица отображения — одна из самых неочевидных вещей, как и Z-координата. Казалось бы, если у нас есть камера, почему бы нам ее не направить на стул и поставить перед ним, и вот он успех, но нет. Не забываем, что для того, чтобы модель отрисовалась, нам нужно вписать ее в тот самый куб, который, к сожалению, двигать нельзя. И как же быть? Да очень просто, мы просто возьмем весь наш трехмерный мир и вместо камеры подвинем и перевернем его. Да, звучит странно, но именно так это и работает. Задумайтесь, во всех играх, в которые вы играли, не камера двигается по миру, а мир вращается и двигается вокруг камеры!
Есть небольшой лайфхак: для того, чтобы быстро построить такую матрицу, мы можем сначала построить матрицу, будто бы мы двигаем именно камеру, а потом инвертировать ее. Если коротко объяснить инверсию матрицы в WebGL, она создает копию ваших преобразований, но наоборот. Например, вы увеличиваете стул в два раза, а инверсная матрица будет его в два раза уменьшать. Подвинули вперед? Инверсная подвинет назад и так далее.
🧶 Что же касается матрицы проекции — это одна из самых сложных в понимании матриц. Она делает так, что пространство как бы сплющивается при ухождении в глубину. Именно за счет нее достигается перспектива (это если мы говорим про перспективную проекцию, существует так же ортографическая проекция, но про нее как-нибудь в другой раз). Здесь же я бы рассказал про Z-координату нашего куба и почему она вообще существует, но про это мы поговорим уже в другом посте, про экранные буфферы.
Постарался разжевать как мог, но я думаю, что не все разберутся с наскока, поэтому жду ваших вопросов и комментариев, будем разбираться вместе!
#rdclr_frontend #WebGL
🪡 Матрица объекта — это самая простая в объяснении из всех трех. Допустим, у нас есть стул (ну, в реалиях WebGL это будет просто пачка вершин и треугольников, но давайте оперировать человеческим языком). И вот у нас появляется задача: нужно этот стул переставить в пространстве. Что мы будем делать? Правильно, запишем в матрицу объекта значения, на которые нам нужно подвинуть наш стул. Хотите повернуть его на 90 градусов? Правильно, запишем в матрицу данные о размерах. Стул слишком большой? Я думаю, можно не продолжать.
🧵 Матрица отображения — одна из самых неочевидных вещей, как и Z-координата. Казалось бы, если у нас есть камера, почему бы нам ее не направить на стул и поставить перед ним, и вот он успех, но нет. Не забываем, что для того, чтобы модель отрисовалась, нам нужно вписать ее в тот самый куб, который, к сожалению, двигать нельзя. И как же быть? Да очень просто, мы просто возьмем весь наш трехмерный мир и вместо камеры подвинем и перевернем его. Да, звучит странно, но именно так это и работает. Задумайтесь, во всех играх, в которые вы играли, не камера двигается по миру, а мир вращается и двигается вокруг камеры!
Есть небольшой лайфхак: для того, чтобы быстро построить такую матрицу, мы можем сначала построить матрицу, будто бы мы двигаем именно камеру, а потом инвертировать ее. Если коротко объяснить инверсию матрицы в WebGL, она создает копию ваших преобразований, но наоборот. Например, вы увеличиваете стул в два раза, а инверсная матрица будет его в два раза уменьшать. Подвинули вперед? Инверсная подвинет назад и так далее.
🧶 Что же касается матрицы проекции — это одна из самых сложных в понимании матриц. Она делает так, что пространство как бы сплющивается при ухождении в глубину. Именно за счет нее достигается перспектива (это если мы говорим про перспективную проекцию, существует так же ортографическая проекция, но про нее как-нибудь в другой раз). Здесь же я бы рассказал про Z-координату нашего куба и почему она вообще существует, но про это мы поговорим уже в другом посте, про экранные буфферы.
Постарался разжевать как мог, но я думаю, что не все разберутся с наскока, поэтому жду ваших вопросов и комментариев, будем разбираться вместе!
#rdclr_frontend #WebGL
🔥15❤3
Типы конвейеров в WebGL — шейдеры/1
Опять начну пост с исторической справки: когда я начинал щупать OpenGL, для общего понимания все предлагали использовать фиксированный конвейер, хотя в это время уже вовсю были распространены шейдеры.
Что же это вообще такое? Давайте разбираться.
Сначала давайте определимся с общим понятием конвейера. 🏭 В нашем случае, конвейер — это некая последовательность действий, которую нам в связке с видяхой нужно совершить, чтобы вывести графику на экран (давайте пока ограничимся той же моделькой стула из предыдущего поста).
Так как OpenGL (как и WebGL) является по сути процедурной стейт-машиной, перед каждой отрисовкой нам нужно объяснить ему, что нам нужно, что нет, какие режимы включить и так далее. Когда мы работаем с фиксированным конвейером, общая последовательность действий такая:
- Установить все три матрицы из прошлого поста - model, view & projection.
- Включить текстурирование, привязать текстуру стула.
- Отправить данные о вершинах на видяху.
- Привязать эти данные в отрисовку.
- Настроить светильники.
- Вызвать отрисовку.
- Отключить светильники, отвязать данные вершин, отключить текстурирование.
И вот эта последовательность действий у нас выполняется каждый кадр для каждой модели. Казалось бы, все логично, но фиксированный конвейер накладывает свои ограничения:
💣 1. Мы ограничены в светильниках, максимум — 8
🔮 2. Мы не можем влиять на отрисовку, все растрирование лежит на зашитых алгоритмах
🧿 3. Сложно сделать красиво, в основном приходится прибегать к хакам
И в какой-то момент ребята подумали, и решили: а почему бы нам не дать разработчикам возможность настраивать рендер целиком? Так появились шейдеры (ждите, ща второй пост).
#rdclr_frontend #WebGL
Опять начну пост с исторической справки: когда я начинал щупать OpenGL, для общего понимания все предлагали использовать фиксированный конвейер, хотя в это время уже вовсю были распространены шейдеры.
Что же это вообще такое? Давайте разбираться.
Сначала давайте определимся с общим понятием конвейера. 🏭 В нашем случае, конвейер — это некая последовательность действий, которую нам в связке с видяхой нужно совершить, чтобы вывести графику на экран (давайте пока ограничимся той же моделькой стула из предыдущего поста).
Так как OpenGL (как и WebGL) является по сути процедурной стейт-машиной, перед каждой отрисовкой нам нужно объяснить ему, что нам нужно, что нет, какие режимы включить и так далее. Когда мы работаем с фиксированным конвейером, общая последовательность действий такая:
- Установить все три матрицы из прошлого поста - model, view & projection.
- Включить текстурирование, привязать текстуру стула.
- Отправить данные о вершинах на видяху.
- Привязать эти данные в отрисовку.
- Настроить светильники.
- Вызвать отрисовку.
- Отключить светильники, отвязать данные вершин, отключить текстурирование.
И вот эта последовательность действий у нас выполняется каждый кадр для каждой модели. Казалось бы, все логично, но фиксированный конвейер накладывает свои ограничения:
💣 1. Мы ограничены в светильниках, максимум — 8
🔮 2. Мы не можем влиять на отрисовку, все растрирование лежит на зашитых алгоритмах
🧿 3. Сложно сделать красиво, в основном приходится прибегать к хакам
И в какой-то момент ребята подумали, и решили: а почему бы нам не дать разработчикам возможность настраивать рендер целиком? Так появились шейдеры (ждите, ща второй пост).
#rdclr_frontend #WebGL
🔥2
Типы конвейеров в WebGL — шейдеры/2
Если коротко, то шейдер — это маленькая программка на C-подобном языке, которая сначала выполняется для каждой вершины, а потом для каждого пикселя модели на экране, и определяет какой цвет должен быть в этом пикселе.
Благодаря шейдерам нам становится доступен огромнейший пласт эффектов, ведь теперь мы не ограничены растрированием со стороны OpenGL.
Однако, шейдерный конвейер автоматически указывает нам, что все прошлые манипуляции со стороны программы становятся устаревшими, и нужно внести некоторые коррективы, которые на самом деле ускорят наш рендер.
💛 Вся матричная математика уезжает на видеокарту — не просто так у нее есть compute-ядра, которые считают перемножения матриц в сотни раз быстрее ЦП, тут и выигрыш.
💙 Освещение целиком и полностью ложится на программиста, так как на пиксели, которые выводит шейдер, не распространяется фиксированный конвейер.
💜 Наложение текстуры тоже теперь должно считаться на видяхе, делается очень просто, но мы можем полностью контролировать какой пиксель с текстуры брать для текущего пикселя.
🤎 Мы можем передавать в шейдеры какие угодно данные: как общие, например текущий цвет освещения, так и повершинные, например текстурная координата этой вершины.
❤️ Вершинный и фрагментный (попиксельный) шейдеры могут обмениваться информацией для удобства рендера.
Но лучше всего будет объяснить это наглядно, чем мы и займемся в следующем посте. 👀
#rdclr_frontend #WebGL
Если коротко, то шейдер — это маленькая программка на C-подобном языке, которая сначала выполняется для каждой вершины, а потом для каждого пикселя модели на экране, и определяет какой цвет должен быть в этом пикселе.
Благодаря шейдерам нам становится доступен огромнейший пласт эффектов, ведь теперь мы не ограничены растрированием со стороны OpenGL.
Однако, шейдерный конвейер автоматически указывает нам, что все прошлые манипуляции со стороны программы становятся устаревшими, и нужно внести некоторые коррективы, которые на самом деле ускорят наш рендер.
💛 Вся матричная математика уезжает на видеокарту — не просто так у нее есть compute-ядра, которые считают перемножения матриц в сотни раз быстрее ЦП, тут и выигрыш.
💙 Освещение целиком и полностью ложится на программиста, так как на пиксели, которые выводит шейдер, не распространяется фиксированный конвейер.
💜 Наложение текстуры тоже теперь должно считаться на видяхе, делается очень просто, но мы можем полностью контролировать какой пиксель с текстуры брать для текущего пикселя.
🤎 Мы можем передавать в шейдеры какие угодно данные: как общие, например текущий цвет освещения, так и повершинные, например текстурная координата этой вершины.
❤️ Вершинный и фрагментный (попиксельный) шейдеры могут обмениваться информацией для удобства рендера.
Но лучше всего будет объяснить это наглядно, чем мы и займемся в следующем посте. 👀
#rdclr_frontend #WebGL
🔥3😱1
Разбираем тени / 1 (простой пример работы шейдеров)
Из-за того, что Blitz3D, с которого я начинал знакомство с графикой, не сильно был способен реализовать какие-либо графические эффекты (а связано это было с тем, что он был довольно старым и использовал под капотом Direct3D 7), меня всегда впечатляли приемы, которые использовали «взрослые» игры, например — тени. 👣
Если мы соберем сцену с улицей, она будет казаться неполной и плоской, так как обычно мы привыкли видеть тень от солнца на всех объектах. И как оказалось, тень от солнца является одним из самых простых эффектов, который можно реализовать с помощью шейдеров. Как же оно работает?
На самом деле, чтобы представить и посчитать тени, мы должны подумать немного нестандартно, и представить: «а что бы мы видели, если бы именно мы были солнцем?» Звучит странно, но именно такой подход является ключом к реализации нашего эффекта. Чтобы полностью понять принцип, мы откажемся от наших мебельных условностей и соберем абстрактную сцену, как на картинке. —>
#rdclr_frontend #WebGL
Из-за того, что Blitz3D, с которого я начинал знакомство с графикой, не сильно был способен реализовать какие-либо графические эффекты (а связано это было с тем, что он был довольно старым и использовал под капотом Direct3D 7), меня всегда впечатляли приемы, которые использовали «взрослые» игры, например — тени. 👣
Если мы соберем сцену с улицей, она будет казаться неполной и плоской, так как обычно мы привыкли видеть тень от солнца на всех объектах. И как оказалось, тень от солнца является одним из самых простых эффектов, который можно реализовать с помощью шейдеров. Как же оно работает?
На самом деле, чтобы представить и посчитать тени, мы должны подумать немного нестандартно, и представить: «а что бы мы видели, если бы именно мы были солнцем?» Звучит странно, но именно такой подход является ключом к реализации нашего эффекта. Чтобы полностью понять принцип, мы откажемся от наших мебельных условностей и соберем абстрактную сцену, как на картинке. —>
#rdclr_frontend #WebGL
❤5
Разбираем тени / 2 (простой пример работы шейдеров)
☀️ За первый проход с камерой нужно поступить немного по-другому: нам нужно поставить ее не туда, где мы хотим, чтобы она была, а именно туда, откуда «светит» солнце.
Мало того, что мы по-особенному ставим камеру, так еще и рисуем не прямо на экран, а в отдельный буфер, который на самом деле является обычной текстурой. Плюсом ко всему, нам не нужен цвет или освещение, нам нужно просто понять, насколько отрисованный пиксель находится далеко от «солнца» — поэтому нам хватит текстуры только с красным каналом, куда мы и пишем расстояние от камеры до пикселя. Если все сделано правильно, мы получим карту теней, или shadow map.
🌪Второй проход — это как раз стандартная отрисовка нашей сцены, но с небольшой оговоркой.
Теперь, когда мы будем рисовать нашу сцену, мы ставим камеру как нам нужно и рисуем все, но только для каждого пикселя нам нужно вычислить другой алгоритм: мы берем каждый пиксель, опять считаем его удаленность от солнца и сравниваем с той самой картой теней. Пиксель находится дальше от солнца, чем значение в карте теней по этим координатам? Значит, наш пиксель находится в тени, так как есть какой-то объект, который находится к солнцу ближе, чем наш пиксель.
Это очень упрощенное описание алгоритма, но оно позволяет понять общий принцип работы теней в графике. Также почти всегда одной картой теней все не ограничивается, и их рендерят 3 или 4 штуки — каждая из них охватывает площадь больше, чем предыдущая. Это сделано для того, чтобы рядом с игроком качество теней было выше, чем грубо говоря в 100 метрах от камеры. 💨
#rdclr_frontend #WebGL
☀️ За первый проход с камерой нужно поступить немного по-другому: нам нужно поставить ее не туда, где мы хотим, чтобы она была, а именно туда, откуда «светит» солнце.
Мало того, что мы по-особенному ставим камеру, так еще и рисуем не прямо на экран, а в отдельный буфер, который на самом деле является обычной текстурой. Плюсом ко всему, нам не нужен цвет или освещение, нам нужно просто понять, насколько отрисованный пиксель находится далеко от «солнца» — поэтому нам хватит текстуры только с красным каналом, куда мы и пишем расстояние от камеры до пикселя. Если все сделано правильно, мы получим карту теней, или shadow map.
🌪Второй проход — это как раз стандартная отрисовка нашей сцены, но с небольшой оговоркой.
Теперь, когда мы будем рисовать нашу сцену, мы ставим камеру как нам нужно и рисуем все, но только для каждого пикселя нам нужно вычислить другой алгоритм: мы берем каждый пиксель, опять считаем его удаленность от солнца и сравниваем с той самой картой теней. Пиксель находится дальше от солнца, чем значение в карте теней по этим координатам? Значит, наш пиксель находится в тени, так как есть какой-то объект, который находится к солнцу ближе, чем наш пиксель.
Это очень упрощенное описание алгоритма, но оно позволяет понять общий принцип работы теней в графике. Также почти всегда одной картой теней все не ограничивается, и их рендерят 3 или 4 штуки — каждая из них охватывает площадь больше, чем предыдущая. Это сделано для того, чтобы рядом с игроком качество теней было выше, чем грубо говоря в 100 метрах от камеры. 💨
#rdclr_frontend #WebGL
👍2🔥2
Про стадии рендера на почитать
В качестве наглядного пособия сколько всего происходит в графике за один кадр, можно изучить отличнейшую статью про стадии рендера в Grand Theft Auto V. 🚗
Хотя игра уже далеко не новая, в сегодняшних реалиях мало что поменялось в подходах рендеринга сцены с отложенным освещением/затенением и экранных эффектов.
#rdclr_frontend #WebGL #read
В качестве наглядного пособия сколько всего происходит в графике за один кадр, можно изучить отличнейшую статью про стадии рендера в Grand Theft Auto V. 🚗
Хотя игра уже далеко не новая, в сегодняшних реалиях мало что поменялось в подходах рендеринга сцены с отложенным освещением/затенением и экранных эффектов.
#rdclr_frontend #WebGL #read
Adrian Courrèges
GTA V - Graphics Study - Adrian Courrèges
The Grand Theft Auto series has come a long way since
the first opus came out back in 1997.
About 2 years ago, Rockstar released GTA V.
The …
the first opus came out back in 1997.
About 2 years ago, Rockstar released GTA V.
The …
А вот все то же самое, только уже в виде видоса, с разделением по дроуколлам. 🛴
#rdclr_frontend #WebGL #read
#rdclr_frontend #WebGL #read