Создавайте снимки 3D-моделей без участия человека с помощью PlayCanvas.

Дэниел — ведущий веб-разработчик в компании Magnopus. Он работает в индустрии уже 20 лет, в течение которых он перешел от разработки переднего плана к стороне дизайна, а затем вернулся к разработке полного стека. За последние несколько лет он занялся разработкой игр, 3Dисмешанной реальности, работая над проектами для The North Face, Wella Professionals и Nokia, и это лишь некоторые из них, а также получил награду Drum. - победивший проект Napapijri.

Почему мы решили изучить автоматизацию захвата рендеринга

В рамках нашего конвейера 3D-активов мы обнаружили необходимость создания миниатюр в ответ на пользовательское событие. Требование: создать согласованную миниатюру для любой загруженной 3D-модели и вернуть изображение для отображения в пользовательском интерфейсе после завершения загрузки.

Принимая во внимание использование PlayCanvas в качестве нашего предпочтительного 3D-движка, имело смысл исследовать автоматизацию процесса захвата внутри движка, а не смотреть наружу, когда возможности были там.

Учитывая все вышеизложенное, как бы мы это реализовали?

Реализация

Хотя цель нашей реализации — автоматизировать отправку 3D-модели для захвата в наш инструмент, нам нужно начать с фиксированной модели, чтобы экспорт работал должным образом.

Краткое изложение шагов, которые мы предпримем для достижения этой цели:

  • Настройте экземпляр PlayCanvas, который визуализирует нашу модель, чтобы показать, что мы будем снимать, а также дополнительную камеру для захвата снимка экрана.
  • Добавьте цель рендеринга и текстуры для хранения данных буфера для захвата содержимого холста и обеспечения возможности извлечения данных.
  • Создайте и примените данные изображения к элементу холста HTML для извлечения.
  • Преобразуйте данные изображения в URL-адрес и загрузите захваченный снимок экрана.

Давайте представим нашу тестовую модель duck.glb из коллекции glTF Sample Models.

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

Для начала нам нужно настроить экземпляр PlayCanvas, который загружает нашу модель Duck, которую можно просмотреть в браузере, с некоторыми элементами управления камерой, чтобы изменить вращение актива для окончательного вывода.

renderer.scene.createCamera({
  name: "camera",
  fov: 0.4,
  clearColor: new Color().fromString(clearColor),
  disableBloom: true,
  position: [0, 0.75, 4],
  rotation: [0, 0, 0, 1],
});
const renderCameraRef = renderer.scene.createCamera({
  name: "renderCamera",
  fov: 0.4,
  clearColor: new Color().fromString(clearColor),
  disableBloom: true,
  position: [0, 0.75, 4],
  rotation: [0, 0, 0, 1],
});
// Ensure it gets rendered first so not to interfere with other cameras
renderCameraRef.value.camera.priority = -1;
const mesh = await renderer.scene.loadMesh({
  name: assetName,
  filename: `${assetName}.glb`,
  url: assetUrl,
  position: [0, 0, 0],
});

Это создаст элемент холста, который имеет полный размер окна нашего браузера и отрисовывает утку внутри нашей среды.

Кроме того, в рамках этого шага мы создали вторую камеру PlayCanvas, которая будет использоваться для захвата изображения и связанных данных пикселей.

Это, плюс кнопка в левом нижнем углу, будет нашим тестовым приложением для экспорта того, что вы видите на холсте, в виде файла PNG.

Далее нам нужно создать цель рендеринга PlayCanvas и несколько текстур для хранения данных буфера, которые будут использоваться, когда дело доходит до сбора данных о пикселях с экрана.

// Create render target
const device = renderer._app.graphicsDevice;
const colorBufferTex = new pc.Texture(renderer._app.graphicsDevice, {
  width: device.width,
  height: device.height,
  format: pc.PIXELFORMAT_R8_G8_B8_A8,
  // @ts-ignore
  autoMipmap: true,
});
const depthBufferTex = new pc.Texture(device, {
  format: pc.PIXELFORMAT_DEPTHSTENCIL,
  width: device.width,
  height: device.height,
  mipmaps: false,
  addressU: pc.ADDRESS_CLAMP_TO_EDGE,
  addressV: pc.ADDRESS_CLAMP_TO_EDGE,
});
colorBufferTex.minFilter = pc.FILTER_LINEAR;
colorBufferTex.magFilter = pc.FILTER_LINEAR;
const renderTarget = new pc.RenderTarget({
  colorBuffer: colorBufferTex,
  depthBuffer: depthBufferTex,
  samples: 4, // Enable anti-alias
});
renderCameraRef.value.camera.renderTarget = renderTarget;

Нам понадобится новый элемент холста для хранения извлеченных данных изображения при подготовке к загрузке в виде изображения. Данные, собранные в буфере текстур и массиве пикселей, будут присвоены 2D-контексту нового холста.

Наконец, после назначения всех этих данных холсту, мы будем вызывать отрисовку изображения в контексте для рендеринга данных изображения.

const cb = renderTarget.colorBuffer;
// Create a canvas context to render the screenshot to
const canvas = window.document.createElement('canvas');
let context: any = canvas.getContext('2d');
canvas.height = cb.height;
canvas.width = cb.width;
// The render is upside down and back to front so we need to correct it
context.globalCompositeOperation = 'copy';
context.setTransform(1, 0, 0, 1, 0, 0);
context.scale(1, -1);
context.translate(0, -canvas.height);
const pixels = new Uint8Array(colorBufferTex.width * colorBufferTex.height * 4);
const colorBuffer = renderTarget.colorBuffer;
const depthBuffer = renderTarget.depthBuffer;
context.save();
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, colorBuffer.width, colorBuffer.height);
context.restore();
const gl = device.gl;
const fb = device.gl.createFramebuffer();
// We are accessing a private property here that has changed between
// Engine v1.51.7 and v1.52.2
// @ts-ignore
const colorGlTexture = colorBuffer.impl ? colorBuffer.impl._glTexture : colorBuffer._glTexture;
// @ts-ignore
const depthGlTexture = depthBuffer.impl ? depthBuffer.impl._glTexture : depthBuffer._glTexture;
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorGlTexture, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.TEXTURE_2D, depthGlTexture, 0);
gl.readPixels(0, 0, colorBuffer.width, colorBuffer.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
gl.deleteFramebuffer(fb);
// first, create a new ImageData to contain our pixels
const imgData = context.createImageData(colorBuffer.width, colorBuffer.height); // width x height
const data = imgData.data;
// Get a pointer to the current location in the image.
const palette = context.getImageData(0, 0, colorBuffer.width, colorBuffer.height); //x,y,w,h
// Wrap your array as a Uint8ClampedArray
palette.data.set(new Uint8ClampedArray(pixels)); // assuming values 0..255, RGBA, pre-mult.
// Repost the data.
context.putImageData(palette, 0, 0);
context.drawImage(canvas, 0, 0);

Наконец, мы извлечем данные изображения из элемента canvas в виде строки base64 с типом mime image/png. Затем замените на image/octet-stream, чтобы изображение можно было загрузить через браузер.

const b64 = canvas.toDataURL(‘image/png’).replace(‘image/png’, ‘image/octet-stream’);
link.setAttribute(‘download’, filename + ‘.png’);
link.setAttribute(‘href’, b64);

Результат:

Теперь это завершено и готово к интеграции в конвейер ресурсов или взаимодействие с пользователем с небольшими изменениями.

Мы удалим триггер скриншота на кнопке и добавим его в функцию инициализации, обновим загрузку сетки, чтобы принять параметр, и по завершении вернем строку b64 для использования в нашем пользовательском интерфейсе.

Теперь, когда у нас есть эта функция и она генерирует эскизы для 3D-моделей, что еще мы можем с ней сделать?

Другие возможные варианты использования в будущем

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

Добавьте режим, позволяющий пользователям кадрировать свой 3D-актив и выбирать, когда делать миниатюру.

Разрешить пользователям захватывать пользовательские портреты аватара и применять их в качестве изображения профиля.

Добавьте режим камеры, чтобы пользователи могли делать снимки трехмерной среды и делиться ими в той же среде, что и самолеты.

Приложения имеют далеко идущие последствия и могут быть использованы для предоставления пользователям увлекательных и мощных возможностей различными способами.

Заключение

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

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