DevGang
Авторизоваться

Как создать генеративные иллюстрации с помощью Three.js

Создайте динамичное, генеративное произведение искусства, используя Three.js и сетчатые структуры, вдохновленное минималистскими геометрическими работами Лигии Кларк.

В этом руководстве мы разработаем генеративное произведение искусства, вдохновленное творчеством выдающейся бразильской художницы Лигии Кларк. Ее картины, основанные на минимализме и геометрии, идеально подходят для интерпретации с помощью сетки и генеративных алгоритмов:

Оригинальная картина Лигии Кларк.
Оригинальная картина Лигии Кларк.

Преимущества использования сетки

Сетки — фундаментальный инструмент дизайна, применяемый от типографики до дизайна интерьеров. Их значение выходит далеко за рамки дизайна, охватывая архитектуру, математику, науку, технологии и изобразительное искусство. Ключевое свойство любой сетки — увеличение числа повторов расширяет возможности, добавляя детализацию и ритм. Например, изображение 2x2 пикселя ограничено 4 цветовыми значениями, в то время как изображение 1080x1080 предоставляет 1 166 400 пикселей для цветовых вариаций.

Настройка проекта

Перед началом кодирования настроим проект и структуру папок. В этом проекте будут использоваться vite, react и react three fiber для удобства и быстрой разработки, но вы можете выбрать любой удобный инструмент.

npm create vite@latest generative-art-with-three -- --template react

После создания проекта с помощью Vite установим Three.js и React Three Fiber, а также соответствующие типы.

cd generative-art-with-three
npm i three @react-three/fiber
npm i -D @types/three

Удалим ненужные файлы, такие как vite.svg (папка public), App.css и папку assets. Создадим папку components в папке src для компонентов проекта. Главный компонент назовем  Lygia.jsx, но вы можете использовать любое имя.

├─ public
├─ src
│  ├─ components
│  │  └─ Lygia.jsx
│  ├─ App.jsx
│  ├─ index.css
│  └─ main.jsx
├─ .gitignore
├─ eslint.config.js
├─ index.html
├─ package-lock.json
├─ package.json
├─ README.md
└─ vite.config.js

Продолжим настройку Three.js/React Three Fiber.

Настройка React Three Fiber

React Three Fiber упрощает настройку, автоматически обрабатывая WebGLRenderer и основные элементы: scene (сцену), camera (камеру), canvas (изменение размера холста) и animation loop (анимационный цикл). Все это заключено в компонент Canvas. Компоненты, добавляемые в Canvas, должны соответствовать Three.js API. Вместо ручного создания экземпляров классов и добавления их в сцену, мы используем их как компоненты React (соблюдая camelCase).

Vanilla Three.js
// Vanilla Three.js

const scene = new Scene()
const mesh = new Mesh(new PlaneGeometry(), new MeshBasicMaterial())
scene.add(mesh)
React Three Fiber
// React Three Fiber

import { Canvas } from "@react-three/fiber";

function App() {
  return (
    <Canvas>
      <mesh>
        <planeGeometry />
        <meshBasicMaterial />
      </mesh>
    </Canvas>
  );
}

export default App;

Для полноэкранного отображения приложения добавим стили в index.css.

index.css
html,
body,
#root {
  height: 100%;
  margin: 0;
}

Запустив приложение командой npm run dev, вы увидите следующее:

Поздравляем! Вы создали самое простое приложение! Перейдём к созданию сетки.

Разработка сетки

После импорта референсного изображения Лигии Кларк в Figma и создания макета, эксперименты показали, что большинство элементов размещаются в сетке 50x86 (без отступов). Хотя существуют более точные методы определения модульной сетки, этого достаточно для нашей задачи. Переведем эту структуру сетки в код в файле Lygia.jsx:

import { useMemo, useRef } from "react";
import { Object3D } from "three";
import { useFrame } from "@react-three/fiber";

const dummy = new Object3D();

const LygiaGrid = ({ width = 50, height = 86 }) => {
  const mesh = useRef();
  const squares = useMemo(() => {
    const temp = [];
    for (let i = 0; i < width; i++) {
      for (let j = 0; j < height; j++) {
        temp.push({
          x: i - width / 2,
          y: j - height / 2,
        });
      }
    }
    return temp;
  }, [width, height]);

  useFrame(() => {
    for (let i = 0; i < squares.length; i++) {
      const { x, y } = squares[i];
      dummy.position.set(x, y, 0);
      dummy.updateMatrix();
      mesh.current.setMatrixAt(i, dummy.matrix);
    }
    mesh.current.instanceMatrix.needsUpdate = true;
  });

  return (
    <instancedMesh ref={mesh} args={[null, null, width * height]}>
      <planeGeometry />
      <meshBasicMaterial wireframe color="black" />
    </instancedMesh>
  );
};

export { LygiaGrid };

Далее разберем каждый элемент кода:

  1. Создайте переменную с именем dummy и назначьте ее объекту Object3D из Three.js. Это позволит нам сохранять позиции и любые другие преобразования. Мы будем использовать ее для передачи всех этих преобразований в сетку. У него нет никакой другой функции, отсюда и название dummy (подробнее об этом позже).
  2. Мы добавляем элементы и высоту сетки в качестве реквизита нашего компонента.
  3. Мы будем использовать перехватчики React useRef, чтобы иметь возможность ссылаться на instancedMesh (подробнее об этом позже)..
  4. Чтобы иметь возможность задать позиции всех наших экземпляров, мы заранее вычисляем их в функции. Мы используем хук useMemo из React, потому что по мере увеличения сложности мы сможем сохранять вычисления между повторными рендерами (он будет обновляться только в том случае, если значения массива зависимостей обновятся [width, height]). Внутри памятки у нас есть два цикла for для определения ширины и высоты, и мы задаем позиции, используя i, чтобы задать позицию x, и j, чтобы задать позицию y. Мы будем делить ширину и высоту на два, чтобы наша сетка элементов была центрирована.
  5. У нас есть два варианта установки позиций: хук useEffect из React или хук useFrame из React Three Fiber. Мы выбрали последний, потому что это цикл рендеринга. Это позволит нам анимировать элементы, на которые ссылаются.
  6. Внутри хука useFrame мы проходим по всем экземплярам с помощью squares.length. Здесь мы деконструируем наши предыдущие x и y для каждого элемента. Мы передаем их нашему фиктивному объекту, а затем используем updateMatrix() для применения изменений.
  7. Наконец , мы возвращаем <instancedMesh/>, который оборачивает наш <planeGeometry/>, который будет нашими квадратами 1×1, и <meshBasicMaterial/> — в противном случае мы бы ничего не увидели. Мы также устанавливаем каркасную опору так, чтобы было видно, что это сетка 50×86 квадратов, а не большой прямоугольник.

Теперь мы можем импортировать наш компонент в наше основное приложение и использовать его внутри компонента <Canvas/>. Чтобы просмотреть всю нашу сетку, нам нужно будет отрегулировать z-позицию камеры до 65.

import { Canvas } from "@react-three/fiber";
import { Lygia } from "./components/Lygia";

function App() {
  return (
    <Canvas camera={{ position: [0, 0, 65] }}>
      <Lygia />
    </Canvas>
  );
}

export default App;

Результат будет выглядеть следующим образом

Деконструкция сетки

Одно из самых сложных — отказаться от усвоенных правил, будь то в искусстве, математике или программировании. В работах Лигии Кларк заметно, что некоторые элементы намеренно выходят за пределы строгой сетки.

Рассмотрим столбцы: всего их 12, при этом столбцы под номерами 2, 4, 7, 8, 10 и 11 меньше остальных (столбцы 1, 3, 5, 6, 9 и 12 — больше). Более того, ширина этих меньших столбцов различна (например, столбец 2 шире столбца 10). Для моделирования этого создадим массив меньших номеров столбцов: `[2, 4, 7, 8, 10, 11]`. Проблема в том, что у нас 50 столбцов, а не 12. Простым решением будет циклическое повторение массива из 12 номеров и использование коэффициента масштабирования для определения ширины столбцов (каждый столбец будет содержать 4.1666 квадратов (50/12)):

const dummy = new Object3D();

const LygiaGrid = ({ width = 50, height = 80, columns = 12 }) => {
  const mesh = useRef();
  const smallColumns = [2, 4, 7, 8, 10, 11];

  const squares = useMemo(() => {
    const temp = [];
    let x = 0;

    for (let i = 0; i < columns; i++) {
      const ratio = width / columns;
      const column = smallColumns.includes(i + 1) ? ratio - 2 : ratio + 2;
      for (let j = 0; j < height; j++) {
        temp.push({
          x: x + column / 2 - width / 2,
          y: j - height / 2,
          scaleX: column,
        });
      }

      x += column;
    }
    return temp;
  }, [width, height]);

  useFrame(() => {
    for (let i = 0; i < squares.length; i++) {
      const { x, y, scaleX } = squares[i];
      dummy.position.set(x, y, 0);
      dummy.scale.set(scaleX, 1, 1);
      dummy.updateMatrix();
      mesh.current.setMatrixAt(i, dummy.matrix);
    }

    mesh.current.instanceMatrix.needsUpdate = true;
  });

  return (
    <instancedMesh ref={mesh} args={[null, null, columns * height]}>
      <planeGeometry />
      <meshBasicMaterial color="red" wireframe />
    </instancedMesh>
  );
};

export { LygiaGrid };

Итак, мы зацикливаем наши столбцы, мы устанавливаем наше ratio равным width сетки, деленной на columns. Затем мы устанавливаем column равным нашему ratio минус 2, если он есть в списке наших маленьких столбцов, или ratio плюс 2, если его там нет. Затем мы делаем то же самое, что и раньше, но наш x немного отличается. Поскольку наши столбцы являются случайными числами, нам нужно суммировать текущую ширину column с x в конце нашего первого цикла:

Для более естественного разрушения сетки используем шум (например, библиотеку Open Simplex Noise, но подойдут и другие).

npm i open-simplex-noise

С учетом шума, цикл for будет выглядеть следующим образом:

import { makeNoise2D } from "open-simplex-noise";

const noise = makeNoise2D(Date.now());

const LygiaGrid = ({ width = 50, height = 86, columns = 12 }) => {
  const mesh = useRef();
  const smallColumns = [2, 4, 7, 8, 10, 11];

  const squares = useMemo(() => {
    const temp = [];
    let x = 0;

    for (let i = 0; i < columns; i++) {
      const n = noise(i, 0) * 5;
      const remainingWidth = width - x;
      const ratio = remainingWidth / (columns - i);
      const column = smallColumns.includes(i + 1)
        ? ratio / MathUtils.mapLinear(n, -1, 1, 3, 4)
        : ratio * MathUtils.mapLinear(n, -1, 1, 1.5, 2);
      const adjustedColumn = i === columns - 1 ? remainingWidth : column;
      for (let j = 0; j < height; j++) {
        temp.push({
          x: x + adjustedColumn / 2 - width / 2,
          y: j - height / 2,
          scaleX: adjustedColumn,
        });
      }

      x += column;
    }
    return temp;
  }, [width, height]);

// Rest of code...

Сначала мы импортируем функцию makeNoise2D из open-simplex-noise, затем создаем переменную noise, которая равна ранее импортированной makeNoise2D с аргументом Date.now(). Теперь мы можем перейти к нашему циклу for.

  1. Мы добавляем постоянную переменную под названием n, которая равна нашей функции noise. Мы передаем в качестве аргумента приращение (i) из нашего цикла и умножаем его на 5, что даст нам больше значений между -1 и 1.
  2. Поскольку мы будем использовать случайные числа, нам нужно отслеживать нашу оставшуюся ширину, которая будет равна нашему remaningWidth, деленному на количество columns минус текущее количество столбцов i.
  3. Далее у нас та же логика, что и раньше, чтобы проверить, находится ли столбец в нашем списке smallColumns, но с небольшим изменением; мы используем шум n. В этом случае используем функцию mapLinear из Three.js MathUtils и сопоставим значение с [-1, 1] с [3, 4] в случае, если столбец находится в наших маленьких столбцах, или с [1.5, 2] в случае, если это не так. Обратите внимание, что мы делим его или умножаем. Попробуйте свои значения.
  4. Наконец, если это последний столбец, мы используем наш remaningWidth.

Теперь остался только один шаг, нам нужно задать высоту нашей строки. Для этого нам просто нужно добавить опору для rows, как мы это делали для columns, и выполнить цикл по ней, а в верхней части useMemo мы можем разделить нашу height на количество rows. Не забудьте, наконец, присвоить ему значение temp в качестве scale и использовать его в useFrame.

const LygiaGrid = ({ width = 50, height = 86, columns = 12, rows = 10 }) => {
...
const squares = useMemo(() => {
    const temp = [];
    let x = 0;
    const row = height / rows;

    for (let i = 0; i < columns; i++) {
      const n = noise(i, 0) * 5;
      const remainingWidth = width - x;
      const ratio = remainingWidth / (columns - i);
      const column = smallColumns.includes(i + 1)
        ? ratio / MathUtils.mapLinear(n, -1, 1, 3, 4)
        : ratio * MathUtils.mapLinear(n, -1, 1, 1.5, 2);
      const adjustedColumn = i === columns - 1 ? remainingWidth : column;
      for (let j = 0; j < rows; j++) {
        temp.push({
          x: x + adjustedColumn / 2 - width / 2,
          y: j * row + row / 2 - height / 2,
          scaleX: adjustedColumn,
          scaleY: row,
        });
      }

      x += column;
    }
    return temp;
  }, [width, height, columns, rows]);

useFrame(() => {
    for (let i = 0; i < squares.length; i++) {
      const { x, y, scaleX, scaleY } = squares[i];
      dummy.position.set(x, y, 0);
      dummy.scale.set(scaleX, scaleY, 1);
      dummy.updateMatrix();
      mesh.current.setMatrixAt(i, dummy.matrix);
    }

    mesh.current.instanceMatrix.needsUpdate = true;
  });
...

Кроме того, помните, что количество наших instanceMesh должно быть равно columns * rows:

<instancedMesh ref={mesh} args={[null, null, columns * rows]}>

После всего этого мы, наконец, увидим ритм более случайного характера. Поздравляю, вы сломали сетку:

Добавление Цвета

Помимо использования масштаба для разбола нашей сетки, мы также можем использовать еще один незаменимый элемент нашего мира; цвет. Для этого мы создадим палитру в нашей сетке и передадим наши цвета нашим экземплярам. Но сначала нам нужно будет извлечь палитру из изображения. Здесь использовался ручной подход; импорт изображения в Figma и использование инструмента eyey-dropper, но вы, вероятно, можете использовать инструмент экстрактора палитры:

Слева направо, вверх и вниз: #B04E26, #007443, #263E66, #CABCA2, #C3C3B7, #8EA39C, #E5C03C, #66857F, #3A5D57.
Слева направо, вверх и вниз: #B04E26, #007443, #263E66, #CABCA2, #C3C3B7, #8EA39C, #E5C03C, #66857F, #3A5D57.

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

//...
import { Color, MathUtils, Object3D } from "three";
//...
const palette =["#B04E26","#007443","#263E66","#CABCA2","#C3C3B7","#8EA39C","#E5C03C","#66857F","#3A5D57",]
const c = new Color();

const LygiaGrid = ({ width = 50, height = 86, columns = 12, rows = 10, palette = palette }) => {
//...
const colors = useMemo(() => {
    const temp = [];
    for (let i = 0; i < columns; i++) {
      for (let j = 0; j < rows; j++) {
        const rand = noise(i, j) * 1.5;
        const colorIndex = Math.floor(
          MathUtils.mapLinear(rand, -1, 1, 0, palette.length - 1)
        );
        const color = c.set(palette[colorIndex]).toArray();
        temp.push(color);
      }
    }
    return new Float32Array(temp.flat());
  }, [columns, rows, palette]);
})
//...
return (
    <instancedMesh ref={mesh} args={[null, null, columns * rows]}>
      <planeGeometry>
        <instancedBufferAttribute
          attach="attributes-color"
          args={[colors, 3]}
        />
      </planeGeometry>
      <meshBasicMaterial vertexColors toneMapped={false} />
    </instancedMesh>
  );

Как мы делали раньше, давайте объясним пункт за пунктом, что здесь происходит:

  1. Обратите внимание, что мы объявили константу c, которая равна Color three.js. Это будет иметь то же применение, что и dummy, но вместо того, чтобы хранить матрицу, мы будем хранить цвет.
  2. Мы используем константу colors для хранения наших рандомизированных цветов.
  3. Мы снова проходим по нашим columns и rows, поэтому длина наших цветов будет равна длине наших экземпляров.
  4. Внутри двухмерного цикла мы создаем случайную переменную, называемую rand, где мы снова используем нашу функцию noise. Здесь мы используем наши переменные i и j из цикла. Мы делаем это, чтобы получить более плавный результат при выборе наших цветов. Если мы умножим его на 1,5, это даст нам больше разнообразия, и это то, что нам нужно.
  5. colorIndex представляет собой переменную, которая будет хранить индекс, который будет идти от 0 до нашей palette.length. Для этого мы снова отображаем наши значения rand с 1 и 1 на 0 и palette.length, которая в данном случае равна 9.
  6. Мы округляем значение, поэтому получаем только целочисленные значения.
  7. Используйте константу c для установки  set текущего цвета. Мы делаем это с помощью palette[colorIndex]. Отсюда мы используем метод three.js Color toArray(), который преобразует шестнадцатеричный цвет в массив [r,g,b].
  8. Сразу после этого мы помещаем цвет в наш temp массив.
  9. После завершения обоих циклов мы возвращаем Float32Array, содержащий наш массив temp в сплющенном виде, поэтому мы получим все цвета как [r,g,b,r,g,b,r,g,b,r,g,b...]
  10. Теперь мы можем использовать наш цветовой массив. Как видите, он используется внутри <planeGeometry> как <instancedBufferAttribute />. У экземпляра буфера есть два свойства: attach="attributes-color" и args={[colors, 3]}. Attach="attributes-color" взаимодействует с внутренней системой шейдеров three.js и будет использоваться для каждого из наших экземпляров. Значением этого атрибута является args={[colors, 3]}, поэтому мы передаем наш массив colors и 3, что указывает на то, что это массив цветов r, g, b.
  11. Наконец, чтобы активировать этот атрибут в наших фрагментных шейдерах, нам нужно установить vertexColors в значение true в нашем <meshBasicMaterial />.

Как только мы все это сделаем, мы получим следующий результат:

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

const colors = useMemo(() => {
  const temp = [];
  
  for (let i = 0; i < columns; i++) {
    for (let j = 0; j < rows; j++) {
      const rand = noise(i, j) * 1.5;
      const range = smallColumns.includes(i + 1)
        ? [0, 4]  // 1
        : [1, palette.length - 1];  // 1
        
      const colorIndex = Math.floor(
        MathUtils.mapLinear(rand, -1.5, 1.5, ...range)
      );
      
      const color = c.set(palette[colorIndex]).toArray();
      temp.push(color);
    }
  }
  
  return new Float32Array(temp.flat());
}, [columns, rows, palette]);

Вот что происходит:

  • Если текущий столбец находится в smallColumns, то диапазон, который мы хотим использовать из нашей палитры, составляет от 0 до 4. А если нет, то: от 1 (без красного) до palette.length - 1.
  • Затем в функции map мы передаем этот новый массив и распространяем его так, чтобы получить 0, 4 или 1, palette.length - 1, в зависимости от выбранной нами логики.

Одно из того, что нужно иметь в виду, это то, что используются фиксированные значения из палитры. Если вы хотите быть более избирательными, вы можете создать список с парами ключ-значение. Вот результат, который мы получили после применения функции double map:

Теперь вы можете итерировать, используя разные числа в функции makeNoise2D. Например, makeNoise2D(10) даст вам результат выше. Поиграйте с разными значениями, чтобы увидеть, что у вас получится!

Добавление графического интерфейса пользователя

Один из лучших способов поэкспериментировать с генеративной системой - это добавление графического пользовательского интерфейса (GUI). В этом разделе мы рассмотрим, как реализовать.

Во-первых, нам нужно будет установить удивительную библиотеку, которая значительно упрощает процесс; leva.

npm i leva

Как только мы его установим, мы сможем использовать его следующим образом:

import { Canvas } from "@react-three/fiber";
import { Lygia } from "./components/Lygia";
import { useControls } from "leva";

function App() {
	const { width, height } = useControls({
	  width: { value: 50, min: 1, max: 224, step: 1 },
	  height: { value: 80, min: 1, max: 224, step: 1 },
	});
	
  return (
    <Canvas camera={{ position: [0, 0, 65] }}>
      <Lygia width={width} height={height} />
    </Canvas>
  );
}

export default App;
  1. Мы импортируем хук useControls из leva.
  2. Используем наш хук внутри приложения и определяем значения ширины и высоты.
  3. Наконец, мы передаем нашу ширину и высоту опорам нашего компонента Lygia.

В правом верхнем углу экрана вы увидите новую панель, где вы можете настроить наши значения с помощью ползунка, как только вы их измените, вы увидите, что сетка меняет свою ширину и/или высоту.

Теперь, когда мы знаем, как это работает, мы можем начать добавлять остальные значения следующими - следующими явами:

import { Canvas } from "@react-three/fiber";
import { Lygia } from "./components/Lygia";
import { useControls } from "leva";

function App() {
	const { width, height, columns, rows, color1, color2, color3, color4, color5, color6, color7, color8, color9 } = useControls({
    width: { value: 50, min: 1, max: 224, step: 1 },
    height: { value: 80, min: 1, max: 224, step: 1 },
    columns: { value: 12, min: 1, max: 500, step: 1 },
    rows: { value: 10, min: 1, max: 500, step: 1 },
    palette: folder({
      color1: "#B04E26",
      color2: "#007443",
      color3: "#263E66",
      color4: "#CABCA2",
      color5: "#C3C3B7",
      color6: "#8EA39C",
      color7: "#E5C03C",
      color8: "#66857F",
      color9: "#3A5D57",
    }),
  });
	
  return (
    <Canvas camera={{ position: [0, 0, 65] }}>
      <Lygia
          width={width}
          height={height}
          columns={columns}
          rows={rows}
          palette={[color1, color2, color3, color4, color5, color6, color7, color8, color9]}
        />
    </Canvas>
  );
}

export default App;

На первый взгляд, это много, но, как и все, что мы делали раньше, все по-другому. Мы объявляем наши строки и столбцы так же, как и для ширины и высоты. Цвета имеют те же шестнадцатеричные значения, что и в нашей палитре, мы просто группируем их, используя функцию folder от leva. После деконструкции мы можем использовать их в качестве переменных для нашего Lygia props. Обратите внимание, что в палитре prop мы используем массив всех цветов, точно так же, как палитра определяется внутри компонента,

Теперь вы увидите что-то вроде следующего изображения:

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

const smallColumns = useMemo(() => {
	const baseColumns = [2, 4, 7, 8, 10, 11];
	
	if (columns <= 12) {
	  return baseColumns;
	}
	
	const additionalColumns = Array.from(
	  { length: Math.floor((columns - 12) / 2) },
	  () => Math.floor(Math.random() * (columns - 12)) + 13
	);
	
	return [...new Set([...baseColumns, ...additionalColumns])].sort(
	  (a, b) => a - b
	);
}, [columns]);

Теперь наша генеративная система готова к использованию.

Применение и практика

Эффективность сетчатой системы обусловлена ее широкими возможностями. Несмотря на простоту, это мощный инструмент, позволяющий создавать бесконечно разнообразные работы. Рекомендуется экспериментировать с сеткой, воспроизводя существующие примеры или создавая собственные. Представлены несколько примеров, которые, как надеемся, послужат источником вдохновения:

Герхард Рихтер

Например, добавив случайность boolean к столбцам и изменив цветовую палитру, можно приблизиться к стилю абстрактных работ Джеральда Рихтера:

ВдохновленныйСерия «Полосы» Герхарда Рихтера, это изображение было создано с помощью нашей сетчатой системы: один столбец на 224 строки, используя ту же палитру, что и картина Лигии.
ВдохновленныйСерия «Полосы» Герхарда Рихтера, это изображение было создано с помощью нашей сетчатой системы: один столбец на 224 строки, используя ту же палитру, что и картина Лигии.
Вдохновленный Герхардом Рихтером4900 Красен, создан с помощью нашей системы сеток: 70 столбцов x 70 строк, с использованием палитры из 24 цветов.
Вдохновленный Герхардом Рихтером4900 Красен, создан с помощью нашей системы сеток: 70 столбцов x 70 строк, с использованием палитры из 24 цветов.

Третье измерение

Цвет может обозначать глубину: синий — даль, жёлтый — близость, красный — исходная точка. Аналогичный подход использовали художники движения De Stijl.

Вдохновленный работамиДе Стиль. Изображение было создано с помощью нашей сетчатой системы: 13 столбцов x 15 строк, с использованием палитры из 5 цветов. Я также изменил камеру с перспективы на орфографическую.
Вдохновленный работамиДе Стиль. Изображение было создано с помощью нашей сетчатой системы: 13 столбцов x 15 строк, с использованием палитры из 5 цветов. Я также изменил камеру с перспективы на орфографическую.

Другие элементы

Как насчет включения кругов, треугольников или линий? Может быть, текстуры? Возможности безграничны - вы можете экспериментировать с различными элементами искусства, дизайна, науки или математики.

Изображение было создано с помощью нашей системы сетки: 11 столбцов x 11 строк, используя только черный цвет и круги.
Изображение было создано с помощью нашей системы сетки: 11 столбцов x 11 строк, используя только черный цвет и круги.

Заключение

В данной статье представлено воссоздание произведений искусства Лигии Кларк и изучение возможностей модульной сетки. Подробно описаны принципы работы с сеткой и методы ее модификации для достижения уникального результата. Приведены примеры произведений искусства, которые могут быть воссозданы с использованием модульной сетки.

Рекомендуется экспериментировать с модульной сеткой, создавая собственные произведения искусства, отражающие индивидуальный стиль. Результаты можно опубликовать и поделиться ими.

Ссылки:

  • Полный код: https://github.com/eduardfossas/codrops-generative-artwork-three
  • Демо: https://tympanus.net/Tutorials/GenerativeArtworkThreejs/

Источник:

#JavaScript #React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу