
Я люблю встроенный граф Obsidian. Первые секунд тридцать.
Пока заметок немного, он действительно помогает увидеть связи. Потом vault подрастает, граф превращается в клубок из сотен точек и линий, а я снова открываю поиск и набираю имя файла руками. Всё вроде бы связано, но пользоваться этой картиной как картой уже трудно.
В какой-то момент мне захотелось не просто видеть, что заметка A связана с заметкой B. Хотелось помнить, что рабочие проекты находятся «вот на той стороне», материалы — чуть левее, а архив — где-то внизу. Не список, не очередное облако, а место.
Так появился CosmoGraph 3D — open-source плагин для Obsidian, который превращает локальный граф знаний в интерактивную планету. Markdown-файлы становятся светящимися узлами, папки — районами, wikilinks — линиями на поверхности. Всё рендерится локально с помощью Three.js и WebGL.
Сразу честно: плагины с 3D-графами для Obsidian уже существуют. Моя идея не в том, чтобы просто добавить третью координату. Отличие CosmoGraph — в ограниченном мире, детерминированном расположении кластеров и процедурном рельефе, который со временем может стать системой ориентиров.
В статье разберу, как из metadataCache получается граф, почему я отказался от force-directed облака, как заметки раскладываются по золотому углу и что пришлось сделать, чтобы Three.js спокойно жил внутри интерфейса Obsidian.
Исходный код открыт: GitHub — n1ghtmare-dev/obsidian-cosmograph. Плагин уже доступен в официальном каталоге Obsidian: установить его можно прямо из Community Plugins — без сборки проекта руками и без ритуалов в полнолуние.
Быстрая навигация
От облака связей к карте местности
У обычного графа есть неприятное свойство: чем больше данных он показывает, тем меньше информации человек способен из него извлечь. Слабосвязанные заметки разлетаются, популярные превращаются в клубки рёбер, а расположение узлов может заметно меняться после очередного запуска симуляции. Официальный Graph view в Obsidian честно показывает структуру связей — проблема начинается, когда я пытаюсь использовать всю эту структуру как навигацию.
Мне хотелось получить четыре свойства:
Ограниченное пространство. У карты должны быть края — или хотя бы понятная форма целиком.
Стабильные районы. Заметки одного верхнеуровневого каталога должны находиться рядом.
Ориентиры. Крупные папки и популярные заметки должны быть видны раньше второстепенных.
Прямая навигация. Найденный узел должен не просто подсвечиваться: сцена должна поворачиваться к нему, а заметка — открываться в Obsidian.
Сфера подошла лучше бесконечной плоскости. Она ограничена, но у неё нет тупиков. Её можно вращать, а кластеры получают устойчивое положение относительно друг друга. Проект из «ещё одного 3D-графа» постепенно превратился в идею knowledge planet — планеты, форму которой создают заметки пользователя.

В проекте специально разделены получение данных, раскладка и рендеринг. Благодаря этому web-прототип и плагин Obsidian используют одну 3D-сцену.
Получаем граф из Obsidian
Внутри плагина граф намеренно имеет простую модель:
export type GraphNode = { id: string; title: string; path: string; group: string; }; export type GraphEdge = { source: string; target: string; };
Для узлов я беру все Markdown-файлы текущего vault. Идентификатором служит полный путь — одного basename недостаточно, потому что в разных каталогах вполне могут существовать два файла README.md или Index.md.
Связи вручную парсить не нужно. Obsidian уже разрешил wikilinks и хранит результат в metadataCache.resolvedLinks:
const files = app.vault.getMarkdownFiles(); const nodes = files.map((file) => ({ id: file.path, title: file.basename, path: file.path, group: displayGroupName(file.path), })); for (const source of files) { const targets = app.metadataCache.resolvedLinks[source.path] ?? {}; for (const targetPath of Object.keys(targets)) { if (!nodeIds.has(targetPath) || targetPath === source.path) continue; const key = [source.path, targetPath].sort().join("\u0000"); if (edgeKeys.has(key)) continue; edgeKeys.add(key); edges.push({ source: source.path, target: targetPath }); } }
Сортировка пары перед формированием ключа делает визуальное ребро неориентированным. Если A ссылается на B, а B — на A, на сфере всё равно рисуется одна линия.
В текущей версии группа определяется по первому каталогу пути. Числовые префиксы вроде 10. Projects убираются из подписи, поэтому можно использовать PARA или другую файловую структуру, не перенося служебную нумерацию в интерфейс.
Это сознательно простая модель. Теги, свойства frontmatter и вложенные уровни каталогов пока не участвуют в кластеризации. Я хочу сначала проверить, помогает ли сама пространственная метафора ориентироваться в большом vault, и только потом усложнять семантику.
Раскладываем заметки по сфере без физической симуляции
Большинство 3D-графов используют force-directed layout. Он универсален, но для моего сценария оказался неидеальным: позиции долго стабилизируются и могут заметно меняться при добавлении одного узла.
В CosmoGraph раскладка детерминирована.
Сначала каждому каталогу назначается направление на сфере. Для первых крупных групп используются заранее подобранные якоря, для остальных — распределение Фибоначчи. Затем вокруг якоря строятся две касательные к сфере:
const tangentA = new THREE.Vector3() .crossVectors( anchor, Math.abs(anchor.y) > 0.88 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0), ) .normalize(); const tangentB = new THREE.Vector3() .crossVectors(anchor, tangentA) .normalize();
Заметки расходятся вокруг центра кластера по золотому углу:
const angle = noteIndex * Math.PI * (3 - Math.sqrt(5)); const spread = Math.min( 1.08, 0.22 + Math.sqrt(noteIndex + 0.2) * 0.18, ); const direction = anchor.clone() .addScaledVector(tangentA, Math.cos(angle) * spread) .addScaledVector(tangentB, Math.sin(angle) * spread) .normalize();
Такой рисунок часто встречается у семян подсолнечника. Он заполняет область равномернее, чем кольца с фиксированным количеством точек, и не требует итерационной симуляции.
После этого направление умножается не на постоянный радиус, а на радиус рельефа в данной точке. Поэтому узлы не висят внутри гор и не проваливаются под поверхность:
const position = direction.multiplyScalar( terrainRadius(direction) + 0.035, );
Кластеры сортируются так, чтобы проекты имели приоритет, а затем — по количеству заметок. Первый кластер получает акцентный цвет и становится начальной точкой фокусировки.

Папка получает якорь на сфере, а её заметки равномерно расходятся вокруг него и прижимаются к локальной высоте рельефа.
Процедурная планета вместо декоративного шара
Гладкая сфера быстро стала похожа на глобус с приклеенными точками. Для ощущения местности понадобился рельеф.
Поверхность строится из SphereGeometry, вершины которой смещаются вдоль нормали. Высоту формируют четыре октавы value noise: крупные формы отвечают за «континенты», средние — за эрозию, мелкие — за зернистость.
function fractalNoise(direction: THREE.Vector3, scale: number) { let amplitude = 0.56; let result = 0; let normalizer = 0; for (let octave = 0; octave < 4; octave += 1) { result += valueNoise(x, y, z) * amplitude; normalizer += amplitude; x = x * 1.93 + 3.1; y = y * 2.07 - 1.7; z = z * 1.89 + 2.4; amplitude *= 0.5; } return result / normalizer; }
Поверх шума накладываются 19 детерминированных кратеров. У каждого есть впадина, вал, неровный край и иногда центральный пик. Генератор использует фиксированный seed, поэтому планета не меняется при каждой перезагрузке.
Стабильность здесь важнее случайного разнообразия. Если рельеф должен работать как пространственный ориентир, гора не может сегодня быть справа от проекта, а завтра исчезнуть.
Для отображения используются два режима:
«Мягкая» — спокойная поверхность на
MeshStandardMaterial;«Сияние» — shader material, частицы, цветовые переходы и bloom.
В radiant-режиме сцена проходит через EffectComposer, RenderPass, UnrealBloomPass и OutputPass. Цвет выводится с ACESFilmicToneMapping. Подписи рисуются отдельным CSS2DRenderer: так текст остаётся обычным DOM и не теряет резкость при масштабировании.
Почему я не показываю подписи у всех заметок
На моём тестовом vault 163 заметки и 39 явных связей. Уже при таком размере попытка подписать каждый узел превращает интерфейс в стену текста.
Поэтому подписи получают:
все узлы-кластеры;
заметки с наибольшим числом связей;
выбранная или найденная заметка.
Количество постоянно подписанных заметок ограничено и зависит от квадратного корня из размера графа:
const rankedNotes = [...data.nodes] .sort((a, b) => degree(b.id) - degree(a.id)) .slice(0, Math.min( 15, Math.max(8, Math.round(Math.sqrt(data.nodes.length) * 2.8)), ));
Это один из примеров, где полезность победила эффектность. Возможность отрисовать сотни labels ещё не означает, что пользователю нужно их одновременно видеть.
Поиск работает похожим образом: неподходящие узлы не обязательно исчезают, а уходят на второй визуальный план. Карта сохраняет контекст, но результат становится заметен.
Фокус — это поворот мира, а не телепортация камеры
При выборе заметки сфера плавно поворачивается нужной стороной к пользователю. Для этого я вычисляю quaternion, переводящий направление узла в точку немного правее центра экрана, и интерполирую между текущим и целевым состоянием.
const localDirection = visual.group.position.clone().normalize(); const desiredDirection = new THREE.Vector3(0.08, -0.02, 1).normalize(); focusStart.copy(root.quaternion); focusEnd.setFromUnitVectors(localDirection, desiredDirection);
Небольшое смещение от центра оставляет место панели с названием, путём и связанными заметками.
Плагин также проверяет prefers-reduced-motion. Если пользователь ограничил анимацию в системе, автоматическое вращение отключается, а фокусировка происходит без длинного перехода.
Встраиваем Three.js в Obsidian
Плагин регистрирует собственный ItemView. Его можно открыть из ribbon или палитры команд. Если вкладка CosmoGraph уже существует, создаётся не новая сцена, а активируется существующая.
Интерфейс помещён в Shadow DOM. Это защищает стили графа от темы Obsidian и одновременно не позволяет стилям плагина случайно перекрасить остальное приложение.
Изменения vault слушаются через события:
this.registerEvent(this.app.vault.on("create", scheduleRefresh)); this.registerEvent(this.app.vault.on("delete", scheduleRefresh)); this.registerEvent(this.app.vault.on("rename", scheduleRefresh)); this.registerEvent(this.app.metadataCache.on("resolved", scheduleRefresh)); this.registerEvent(this.app.metadataCache.on("changed", scheduleRefresh));
События объединяются debounce-интервалом 450 мс. Это защищает сцену от нескольких перестроений, когда Obsidian последовательно обновляет файл и metadata cache.
Здесь же находится одна из текущих технических задолженностей: после изменения плагин пересобирает весь граф. Для сотен заметок это приемлемо, но для vault на десятки тысяч файлов понадобится инкрементальный индекс и частичное обновление GPU-буферов. Этой оптимизации я планирую посвятить отдельную статью с измерениями, а не обещаниями.
При закрытии view отменяется requestAnimationFrame, удаляются обработчики и ResizeObserver, освобождаются geometry, material, textures, composer и WebGL renderer. Для плагина, вкладку которого пользователь может многократно открывать и закрывать, это не косметика: забытый render loop продолжит потреблять CPU и GPU даже после исчезновения интерфейса.

Так выглядит нативный ItemView: слева — группы и поиск, справа — сама сцена, снизу — режимы отображения и фокус.
Локальные заметки остаются локальными
CosmoGraph не отправляет содержимое vault на сервер. Нативный плагин получает файлы через API Obsidian и строит граф в памяти процесса.
В репозитории также сохранился отдельный web-прототип. Он читает выбранную пользователем папку через браузерный FileList, самостоятельно извлекает [[wikilinks]] и использует тот же рендерер. Эта версия была визуальной лабораторией до появления полноценного ItemView.
Разделение оказалось полезным: renderer не зависит от API Obsidian, поэтому внешний вид можно быстро разрабатывать в Vite, а интеграционный слой отвечает только за vault, команды, настройки и открытие заметок.
Что уже работает, а что пока нет
Текущая beta-версия умеет:
строить граф из реального Obsidian vault;
группировать заметки по верхним каталогам;
отображать wikilinks;
искать и фокусировать узлы;
открывать заметки из 3D-сцены;
обновляться при создании, переименовании и удалении файлов;
переключать спокойный и светящийся режимы;
скрывать UI и оставлять только сцену.
Открытые задачи тоже существенные:
проверить поведение на действительно больших vault;
перейти от полной перестройки к инкрементальным обновлениям;
добавить настройки кластеров, цветов, подписей и качества;
улучшить touch-управление и мобильную производительность;
понять, какие пространственные ориентиры действительно помогают памяти, а какие остаются декорацией.
Последний пункт для меня самый интересный. Красивая визуализация ещё не становится рабочим инструментом. Это можно проверить только на реальных хранилищах и реальных сценариях навигации.
Где заканчивается красивая демка
У 3D-визуализации есть опасное свойство: красивый скриншот очень легко принять за готовый продукт. Особенно когда всё светится, медленно вращается и выглядит так, будто сейчас начнёт предсказывать будущее.
CosmoGraph уже можно использовать как навигацию: найти заметку, повернуть к ней сферу, посмотреть ближайшие связи и открыть исходный Markdown-файл. При этом плагин только читает данные vault, строит модель в памяти и ничего не отправляет наружу.
Но я пока не готов утверждать, что планета объективно удобнее обычного поиска. Это ещё предстоит проверить на больших хранилищах и реальных сценариях. Сейчас граф полностью перестраивается после изменений, семантика заметок не учитывается, а мобильный интерфейс остаётся экспериментальным.
Поэтому следующий важный этап — не добавить ещё один слой свечения. Сначала нужны измерения: время до первого интерактивного кадра, поведение на тысячах заметок и понимание того, какие ориентиры пользователь действительно запоминает.
Установка CosmoGraph 3D за минуту
CosmoGraph 3D уже есть в официальном каталоге Obsidian. Установка занимает примерно минуту:
Открыть Settings → Community plugins → Browse.
Найти CosmoGraph 3D.
Нажать Install, а затем Enable.
Можно перейти и сразу на карточку плагина. BRAT оставлен как отдельный beta-канал для тех, кто хочет получать экспериментальные версии раньше стабильных релизов.
Репозиторий: github.com/n1ghtmare-dev/obsidian-cosmograph
Если идея вам близка, поставьте репозиторию star — для небольшого open-source проекта это простой и вполне осязаемый сигнал, что работу стоит продолжать. Но ещё полезнее будет issue с размером вашего vault, скриншотом результата и описанием того, где навигация перестала быть понятной.
Во второй части я хочу инструментировать весь cold start — от открытия ItemView до первого интерактивного кадра — и попробовать уложить запуск графа в 0,5 секунды на тестовом vault. Если получится, покажу точное «было/стало». Если нет — всё равно покажу, куда на самом деле ушло время и какие оптимизации оказались бесполезными.

