iArt.ai
AI 모션 그래픽 소프트웨어, 누구나 사용할 수 있는 After Effects 대안, 텍스트 프롬프트로 몇 분 안에 애니메이션·설명 영상 제작
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Stylised 3D landscapes, generated in real time from real geographic data.
WorldPaint turns real geographic data — terrain, roads, land use, buildings
and vegetation — into stylized, believable 3D landscapes in real time.
It is built primarily for a viewpoint that moves along a road, not for a
free-roaming terrain generator: it streams the world around a point, keeps a
short lead so what's coming into view is always ready, and pays its
performance budget where a road-level camera actually looks.README 전문 보기 ↗Stylised 3D landscapes, generated in real time from real geographic data.
WorldPaint turns real geographic data — terrain, roads, land use, buildings and vegetation — into stylized, believable 3D landscapes in real time.
It is built primarily for a viewpoint that moves along a road, not for a free-roaming terrain generator: it streams the world around a point, keeps a short lead so what's coming into view is always ready, and pays its performance budget where a road-level camera actually looks.
Give it a longitude and a latitude and a three.js scene. It reads public elevation tiles and OpenStreetMap vector tiles, and builds the ground, the roads, the water, the buildings, the woods, the crops, the road furniture and the sky around that point — procedurally, with no downloaded assets, no baked meshes and no build step.
Philosophy, in short:
Status: experimental, early stage. This code has been running inside a
single application for a while and is now being lifted out into its own
project. The public API (createWorld) is recent and will keep moving before
1.0. Read Status below before depending on it.
git clone https://github.com/jbmvl/worldpaint.git
cd worldpaint
npm install
npm test
npm run demo
opens a standalone demo (plain three.js, no framework) at
http://localhost:4173/demo/ — free-fly keyboard navigation, click to
teleport, an "show object names" checkbox and a place search field. The same
demo is also hosted at https://jbmvl.github.io/worldpaint/demo/, no install
needed. See demo/README.md for details. Short of that,
the other way to see WorldPaint running is through an application that
consumes it, such as the createWorld example below.
src/
core/ geography and low-level primitives — lng/lat ↔ tile/metre
conversion, elevation field, vector-tile fetching, colour
terrain/ the ground mesh: the terrain bubble, its material, the
ground-class map, cutting roads into it
layers/ everything built on top of the terrain — roads, bridges and
tunnels, street kerbing, buildings, gardens, vegetation,
crops, road furniture, the road corridor every other layer
stops at, and the geometry helpers they share
materials/ procedural textures and shared materials
environment/ sky, sun, shadows, fog, weather — the optional lighting rig
inspect/ debug helpers for labelling what's on screen
themes/ the art direction: palettes, silhouettes, profiles — the one
file a fork changes to look different
worldComposer.js orchestrates the layers in a fixed build order
world.js the public entry point: createWorld() and its five verbs
index.js the public surface — what an application may import
The engine (composition rules, performance budgets, build order) and the art
direction (themes/default.js) are deliberately separate — see
Art direction below.
import * as THREE from 'three';
import { Sky } from 'three/examples/jsm/objects/Sky.js';
import { createWorld } from 'worldpaint';
const scene = new THREE.Scene();
const world = createWorld({
THREE,
scene,
// Vector tiles are optional: without them you get bare relief.
vector: { tiles: ['https://…/{z}/{x}/{y}.pbf'], maxZoom: 14 },
// The sky is optional too: without it, you light the scene yourself.
sky: { Sky },
});
await world.setCenter(2.3522, 48.8566);
await world.refresh(2.3522, 48.8566, { force: true });
function frame(delta, camera) {
const at = { x: camera.position.x, y: 0, z: camera.position.z };
world.advance(delta, at);
const paint = world.updateSky({
camera,
date: new Date(),
lng: 2.3522,
lat: 48.8566,
// Optional. Omitted, the last one is carried over; never set, it is an
// ordinary sky — exactly the render you get without ever mentioning weather.
weather: { cloudCover: 0.9, cloudDensity: 0.85, precipitation: 0.5, wind: 0.4 },
});
renderer.setClearColor(paint.clearColor, 1);
renderer.render(scene, camera);
}
The application owns the renderer, the scene, the camera, the clock and the
position. The generator only dresses the point it is shown — including tone
mapping: the light rig intentionally goes above 1 (a night that reads as
truly black is illegible), and NoToneMapping — three's default — clips
that to flat white instead of rolling it off. Set
renderer.toneMapping = THREE.ACESFilmicToneMapping (or whatever curve the
application already uses) rather than leaving it at the default.
Le transport vectoriel peut être fourni par vector.fetchTile(url, options),
compatible avec fetch et renvoyant une Response. Une application peut ainsi
partager les octets et les requêtes en cours avec sa carte, sans coupler le
générateur à celle-ci. Ce transport doit respecter options.signal.
Les loaders de tuiles annulent leurs lectures à dispose() et après 15 secondes ;
le cache de données décodées reste propre à chaque source.
setCenter(lng, lat) | move the terrain bubble |
refresh(lng, lat, {force}) | rebuild everything that comes from vector data |
advance(delta, at) | one frame of work: planting queues, crops, animation |
tunnelAt(x, z, y) | road floor and vault crown at a scene point, or null outside any covered structure — what a chase camera needs to stay under the roof. The floor is the road roadPositionAt reads; y, the subject's scene height, picks the level where two structures overlap |
setSightline(from, to, {fromRadius, toRadius}) | dither away the trees between the camera and the followed subject; call every frame, null turns it off. Street furniture carries the dithering shader (a discard) only on meshes the line crosses |
updateSky({camera, date, lng, lat, weather}) | advance the hour and the weather; returns the night mix, the wetness and the clear colour |
dispose() | release everything that was allocated |
updateSky only exists if a sky was requested. Without one, the generator
poses no light: the application lights the scene as it sees fit.
The sky is ready as soon as createWorld returns; the landscape takes a few
seconds of tiles. The dome also sits on SKY_LAYER, so a camera restricted to
it sees the sky alone — never the terrain appearing piece by piece:
camera.layers.set(SKY_LAYER); // while setCenter/refresh run
// … render frames with updateSky …
camera.layers.set(0); // landscape ready
When the vector source is not known yet, mount without vector and plug it
in later with world.setVector({ tiles, maxZoom }), then refresh(…, { force: true }).
Everything above is a function of place: the same data renders the same landscape. One verb is not, and it is deliberately apart:
world.crossFauna({
kind: 'deer', // any of FAUNA_KINDS
at: { x: camera.position.x, z: camera.position.z },
forward: { x: dir.x, z: dir.z }, // where the camera looks, flat
distanceM: 55, // how far ahead it cuts across
side: 1, // which side it comes from (1 or -1)
});
An animal walks out of one side of the view, runs across the observer's path, and stops on the other side. It is an event, not scenery: the application picks the species and the moment, the animal is not placed in the world, and coming back later will not find it. Use it for the random events of a game — something crossing the road in front of you.
world.perchSwallows(lng, lat) is its counterpart on the overhead lines: it
sets a row of swallows on the nearest wire span (poles only line rural main
and secondary roads) and returns the middle of the row with its distance, or
null when no line is strung nearby. They take off as the observer comes
within 30 m, and are then forgotten.
world.raiseStarlings(lng, lat, { forward }) lifts a flock of starlings out
of a treetop on the nearest wood edge (a roost if one is within 450 m, else
preferably in the direction of forward), and returns where it starts from,
or null with no wood nearby.
Deer, goats and foxes bound; everything else trots. That is decided per species
(FAUNA_SPECIES[kind].bound) and blended in with speed, so a walking deer
still moves diagonally.
world.roadPositionAt(lng, lat, heightAboveGround, { aheadLng, aheadLat, y });
world.bubble.toScenePosition(lng, lat, heightAboveGround) always answers on
the terrain — the natural ground, cut where a road is carved into it. That is
wrong for an embankment or a bridge: the road there sits above the terrain it
crosses, not at its height. roadPositionAt answers the same question but on
the road's own platform when one passes through the point, falling back to
toScenePosition off-road. heightAboveGround keeps the same meaning either
way. The rendered paved surface adds the technical offset ROAD_LIFT_M
(2 cm) above that platform. To place an object exactly on the asphalt, add
this exported constant to its height instead of hardcoding an offset.
The answer is the rendered surface, and it is continuous: on a junction it is the junction slab itself, not the platform of one of the roads that meet there; where a track crosses a road at grade without a junction, it is the higher of the two. A vehicle can therefore read its pitch and roll from a few points under its wheels without the seams between roads showing up as jolts.
At a grade-separated crossing two roads can cover the same point in plan —
the one the caller means, and the one it passes over or under. aheadLng/
aheadLat, a point a few metres ahead along the direction of travel, sets
aside the roads that do not run in that direction; among the rest, the one the
point is furthest inside wins, exactly as without them.
Heading alone cannot tell a bridge from a road it crosses at a skew: below
seventy degrees, the road underneath also runs "ahead". y, the vehicle's own
scene height (the answer of the previous frame, heightAboveGround included),
keeps it on its level: a road more than a metre above or below it only wins
when nothing is at its level, and among the roads that are, the edge rule
stays the only judge — the answer remains continuous. A vehicle that reads its
wheels around a main point should pass them that point's y as well, so all
of them read the same level.
world.roadsideAt(lng, lat, { side, offset, heightAboveGround, aheadLng, aheadLat });
roadsideAt answers the question of an object placed beside the road — a
sign, a banner, a spectator: the point on the edge of the nearest rendered
road, offset metres beyond it, in scene units. side is 1 for the right of
the direction of travel given by aheadLng/aheadLat, -1 for the left, and
0 (default) for whichever side the queried point lies on. The result carries
the road's tangent (along travel) and normal (from the road towards the
point) so the object can face the road; its height is the road platform while
the point still falls on it (embankment, bridge), the terrain otherwise. It
returns null when no road is within radius.
world.roadLaneAt(lng, lat, { aheadLng, aheadLat });
roadLaneAt answers the question of a mobile riding on the road: the cross
section of the nearest rendered road, as painted. usable is the rideable
half-width (to the inner edge of the edge line, or the carriageway edge);
carriagewayWidth excludes the shoulder; divided is true when the centre
line is marked, and laneWidth is then one half of the carriageway —
otherwise (one-way, or no centre line) the whole of it. own and opposite
are lateral ranges { inner, outer } measured from the queried point, positive
to the right of travel: the mobile's own lane, and the oncoming one when
nothing forbids it (two-way without centre line), null otherwise. oneway is
1 along travel, -1 against it, 0 two-way or unknown; offsetM is the
queried point's offset from the rendered axis; distance its distance to it.
It returns null when no road is within radius.
Weather is state, not art direction: it changes as you go, so it travels
with the hour through updateSky rather than living in the theme. Cloud cover
and density, precipitation (rain or snow) and its intensity, wind and haze each
drive the whole rig at once — the sun dims and loses its warmth under cloud,
shadows fade out rather than snapping off, fog thickens and greys, foliage
sways harder and faster, roads and ground darken when wet, and rain or snow
falls in a box that follows the viewpoint. See src/environment/weather.js for
what each coefficient does and why.
The engine never fetches the weather: it only fetches terrain and vector tiles, and knows no service. Where the state comes from — a real forecast, a simulation, a player setting — is the application's business. The demo drives it from sliders; a real application might use a free, keyless source such as Open-Meteo.
The default (DEFAULT_WEATHER) is an ordinary sky, and every modulation is
written to be the identity on it: an application that never mentions weather
sees exactly the landscape it saw before the feature existed.
three is a peer dependency and is injected, never imported: two copies
of three in one page share neither their constants nor their prototypes.
reach)reach is the one option that restricts what is built: a radius in metres
around the viewer. Everything is bounded by it — the terrain tiles and the
vector tiles that touch it, the road carving, the ground map, the trees, and
every layer's radius (it never extends one). It follows the viewer, and it can
change at any time with world.setReach(metres | null), which takes effect on
the next refresh or mountAt. WorldPaint does not know why the world is
small: the application decides, and uses the same option in every case.
// A fixed scene — a start line, a finish, the replay of an event.
const world = createWorld({ THREE, scene, reach: 120, sky: { Sky, fogRadius: 120 } });
world.setVector({ tiles, maxZoom: 14 });
await world.mountAt(lng, lat);
// A small world that follows someone: recentre it like a full bubble.
await world.setCenter(lng, lat);
await world.refresh(lng, lat);
// Near first: a quick first frame, then the rest while rendering.
world.setReach(300);
await world.mountAt(lng, lat);
world.setReach(null);
world.refresh(lng, lat, { force: true });
Without reach, the whole bubble is built. mountAt meshes the terrain once,
after the roads have carved it, and the returned promise resolves on a complete
scene: tiles meshed, trees sown. mountAt(lng, lat, { budgetMs }) yields to the
browser every budgetMs (8 ms by default; Infinity never yields). With a
reach, the décor is rebuilt at a shorter step so the viewer never nears its
edge. Grass and crops are still scattered around the camera each frame. The
far relief is only shown without a reach. The fog is the application's: pass
sky.fogRadius, or call world.environment.setFogRadius, to hide what lies
beyond the reach. Building near first costs more in total than one full mount
(the near part is built twice), but the first frame comes much sooner.
world.setGenerationPaused(true) suspends cooperative construction and terrain
preparation at the next checkpoint. Existing geometry stays available to render;
resuming with false continues the same generator and queues, without rebuilding
completed layers. world.generationPaused reads that state. Disposal releases
pending waits. Rendering and animation remain under the application's control.
Use this when a menu covers the scene or the document is hidden.
world.lastGenerationAt is the performance.now() of the last construction
step: a frame started before it may have waited for it, which an adaptive
quality loop should not mistake for rendering cost.
Geometry conversion, road graph/junction/profile processing and furniture placement and dense vector-feature visits yield within their work. The budget schedules checkpoints; it is not a hard bound on browser canvas readbacks, shader compilation or GPU uploads. Prefer one cooperative full mount when interface responsiveness matters: a near mount followed by a full rebuild repeats work in the area already built.
On a recorded place (demo/lab/mount.html), a 120 m scene takes about half a
second from createWorld to the first frame in headless Chrome, against four
seconds for the full bubble. Shader compilation, on the first render, is a large
share of that: an application that knows it will mount a scene can warm it with
renderer.compileAsync.
view.farBlockSize)Beyond the bubble, a coarse sheet of relief — 15 bubble tiles wide by default,
about 6.5 km of radius, one flat colour, no decor — carries the silhouette of
the slopes. It is only built where the elevations under it span more than
200 m, and the fog then moves out towards its edge (fully from 500 m of
relief); on flat land nothing is built and the fog stays on the bubble. Pass
view: { farBlockSize: 0 } to remove it, or sky.fogRadius to keep the fog
where you put it. It is only shown without a reach. The camera's far plane
must reach the sheet.
detail.budget)Without any setting, the detail tightens in towns: the layers' radius stops
where the built walls around the viewer add up to detail.budget metres
(60 km by default, never under 300 m), and the rebuild step shrinks with it. A
village never reaches the budget and keeps its full reach. budget: Infinity
turns this off.
detail.radius)createWorld({ detail: { radius: 375 } }) bounds the objects — roads,
buildings, furniture, trees — to that radius everywhere, and the rebuild step
shrinks with it. Unlike reach, the terrain, the ground map (fields, woods,
water painted on the ground) and the far relief keep the whole bubble: the
landscape stays on the horizon, only the costly generation is bounded. The wall
budget still tightens buildings further in towns.
Everything that decides what things look like — palettes, tree
silhouettes, stand types, crop looks, road profiles, furniture colours —
lives in one file, src/themes/default.js, exported as defaultTheme. The
rest of src/ decides how a landscape is composed: where a tree goes, how
a road cuts into terrain, when a tile is replanted.
Pass your own slices to change the look. Slices replace wholesale — no deep merge, so what you read is what you get:
import { createWorld, defaultTheme } from 'worldpaint';
createWorld({
THREE,
scene,
theme: {
towns: [{ name: 'adobe', walls: ['#d9c3a5'], roofs: ['#9c6b4a'], roofShapes: ['flat'] }],
windows: { ...defaultTheme.windows, litShare: 0.6 },
},
});
The resolved theme is frozen and handed down to the layers, which each keep it on their own instance. Nothing holds it globally, so two worlds with different themes can live side by side on one page.
Performance budgets and streaming ranges are deliberately not in the theme: they are frames per second, not taste. Neither are composition rules — the village-palette lattice, the forest-stand lattice, reading field edges out of the ground-class map. Those are the engine, and they stay open.
The shipped theme is not a neutral sample. It is a European, broadly French countryside seen from a road, and that is on purpose — an engine that cannot paint anything convinces nobody.
src/core/regions.js
for the records, src/core/regionInterpretation.js for the closed vocabulary,
and docs/regions.md for how to write a region. Outside any covered region
the scenery switches off — nothing is fetched, nothing is placed, you see
the sky and nothing below it. That is deliberate: scenery drawn from the
default lists looks like scenery, so nobody notices it is wrong. Forcing a
region (world.setRegion) turns it back on anywhere.docs/inventaire.md is the inventory: for every object in the scene — surface,
tree, building, road fitting, animal, landmark — it says whether it is read
from the tiles, inferred from what was read, or invented under stated
conditions, with the actual thresholds and regional tables. docs/surfaces.md
covers the ground in detail, docs/regions.md the natural regions.
Attribution for whatever you display is your responsibility;
world.attribution gives the string for the defaults. WorldPaint expects
the OpenMapTiles schema specifically — a different vector-tile schema will
need its own mapping in layers/.
createWorld and the five verbs) is recent and may still
change before a 1.0.npm run demo locally (see
demo/README.md) — free-fly keyboard navigation, click-to-teleport, an
object-name overlay and a place search. It is a thin application, not a
reference UI.The immediate goal is improving the landscape's perceptual credibility — still within the composition philosophy above, not by adding realism for its own sake. Areas identified but not yet built:
None of the above is implemented today — this section is a direction, not a feature list.
npm test
722 tests, plain node --test, no browser, no build.
See CONTRIBUTING.md.
MIT — see LICENSE.
Les mesures CPU par couche et le banc de terrain sont décrits dans docs/performance.md.