{"id":32828987,"url":"https://github.com/charles-masse/portfolio","last_synced_at":"2026-05-15T11:36:02.963Z","repository":{"id":321354075,"uuid":"1083465472","full_name":"charles-masse/portfolio","owner":"charles-masse","description":"interactive crowd portfolio","archived":false,"fork":false,"pushed_at":"2026-05-04T04:10:06.000Z","size":31700,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-04T06:17:20.241Z","etag":null,"topics":["artificial-intelligence","crowd-simulation","glsl-shader","javascript","portfolio-page","rvo2","three-js","yuka"],"latest_commit_sha":null,"homepage":"https://charles-masse.github.io/portfolio/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/charles-masse.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-26T04:25:35.000Z","updated_at":"2026-05-04T04:14:50.000Z","dependencies_parsed_at":"2025-10-29T08:30:05.999Z","dependency_job_id":null,"html_url":"https://github.com/charles-masse/portfolio","commit_stats":null,"previous_names":["charles-masse/portfolio"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/charles-masse/portfolio","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charles-masse%2Fportfolio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charles-masse%2Fportfolio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charles-masse%2Fportfolio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charles-masse%2Fportfolio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/charles-masse","download_url":"https://codeload.github.com/charles-masse/portfolio/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charles-masse%2Fportfolio/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33065701,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-15T11:35:32.926Z","status":"ssl_error","status_checked_at":"2026-05-15T11:35:31.362Z","response_time":103,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["artificial-intelligence","crowd-simulation","glsl-shader","javascript","portfolio-page","rvo2","three-js","yuka"],"created_at":"2025-11-07T19:00:56.337Z","updated_at":"2026-05-15T11:36:02.957Z","avatar_url":"https://github.com/charles-masse.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Portfolio [WIP] ![Dynamic Regex Badge](https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fgithub.com%2Fcharles-masse%2Fportfolio%2Fblob%2Fmain%2Findex.html\u0026search=Version%5Cs(%5B0-9%5D%2B%5C.%5B0-9%5D%2B%5Ba-z%5D%3F%5Cs)\u0026replace=%241\u0026style=flat\u0026label=Version\u0026labelColor=%23333A41) [![Deploy to GitHub Pages](https://github.com/charles-masse/portfolio/actions/workflows/deploy.yml/badge.svg)](https://github.com/charles-masse/portfolio/actions/workflows/deploy.yml)\nI always wanted an interactive Crowd portfolio where, instead of being just a few seconds of curated footage, visitors could actually *stress-test* the behaviors I usually use in my crowds. I feel it would let visitors see, firsthand, the intricacy in the work I'm producing.\n\n## Goals\n- [Display a decent amount of visually distinct characters on screen](#How-to-manage-lots-of-characters-on-screen)\n- [Create an interesting environment for these characters to interact with](#An-interesting-environment)\n\n## How to manage lots of characters on screen\nObviously, it's Crowd—you need a lot of characters, but it's also the scariest part.\nMy website needs to run well on most computers and smartphones because the last thing I want is to lose an opportunity as a result of a laggy portfolio on a recruiter's phone.\n\n\nHere's what I did to overcome these hurdles :\n\n### Stylized over Realistic\nThe *Uncanny Valley* is a tough hill to climb and the results often age badly. Instead, I opted for a stylized look as I feel, if done right, doesn't age as quickly and doesn't require as much resources to make it look nice. \n\nI got really inspired by **Valve**'s promotional material for \u003cins\u003ePortal 2\u003c/ins\u003e : simple pictogram characters, but full of personality. They can be achieved with a low amount of polygons, variations can be a simple texture swap and they don't need to be affected by lights since they're a flat black color.\n\n![Pictogram character from the Portal 2 trailers waving at the camera](/../gh-images/portal2.gif)\n\nFor the environment, I went with something similar. Super minimalistic shapes to keep it low poly and white textures not to distract too much from the crowd. The only compromise I opted for is enabling shadows to create depth and contrast.\n\n![Reference of a minimalist city with hard shadows](/../gh-images/city.jpg)\n\n*Reference image by Ambrose Yu*\n\n### Instanced characters\nA more technical way to handle a large number of objects in **Three.js** is to use a single *Instanced Mesh* with X instances, instead of cloning the same object X number of times.\n\nHere's the results of a test I did on my machine with 10 000 cloned [Suzannes](https://commons.wikimedia.org/wiki/File:Suzanne.stl#/media/File:Suzanne.stl) vs 10 000 instances of Suzanne:\n\n|                     | Cloned (min-max)  | Instanced (min-max) |\n|---------------------|-------------------|---------------------|\n| **FPS**             | 13-23             | 39-43               |\n| **Frame Time (ms)** | 3-776             | 2-81                |\n| **Memory (mb)**     | 130-282           | 9-20                |\n\nAs you can see, the max FPS is nearly doubled with the instances, they load 10x faster and use less than 10% of the cloned memory.\n\nThe only problem, every instanced characters must look the same and play the same animation...\n\n### Custom Shader\nLike I mentioned before, instances comes with a major constraint—they all need to be identical. Pretty hard to create an interesting crowd when everyone looks and acts the same. This is where *GL Shaders* come into play.\n\nBy overriding each agent's vertices position in the *Vertex Shader*, you can have everyone play a different clip. Additionally, to lower the CPU's load and since we're already using the GPU with the shader, the animations can be read through a *Vertex Animation Texture*. A texture where each pixel's RGB values represent a XYZ transforms. :\n\n![Vertex animation texture](/public/Pedestrians/VAT.png)\n\nLast but not least, the variations can be handled inside the *Fragment Shader* by loading different textures and alphas depending on the selected variation.\n\n### KdTree\nFinally, crowd simulation is not just about optimizing the geo. During steering calculations, agents have to look around to see what their neighbors are doing. Normally, that agent would look through the whole list of agents, compare their distance to theirs and return the agents that fit in their detection radius—thus making each frame's calculations more and more complex as the number of agents increase.\n\n![KdTree](https://upload.wikimedia.org/wikipedia/commons/b/bf/Kdtree_2d.svg)\n\n## An interesting environment\n\n\n### Showcase the typical crowd behaviors\n- Pedestrians\n- Spectators\n- Flock\n- Traffic\n\n### Be interesting on phones and computers\n\n\n## Special Thanks\n- [Mugen87](https://github.com/Mugen87) for the [Yuka Game AI library](https://github.com/Mugen87/yuka)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharles-masse%2Fportfolio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcharles-masse%2Fportfolio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharles-masse%2Fportfolio/lists"}