Edit in JSFiddle

// adapted from https://medium.com/@necsoft/three-js-101-hello-world-part-1-443207b1ebe1
let size = 128;
let scale = 4;

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, 1, 0.1, 1000 );
camera.position.z = 4;

/*
 * we construct a canvas manually so we can specify that
 * we want a webgl2 context
 */
var canvas = document.createElement( 'canvas' );
var context = canvas.getContext( 'webgl2', { alpha: false } );
var renderer = new THREE.WebGLRenderer({antialias:false, canvas: canvas, context: context });

renderer.setSize( size, size );

document.body.appendChild( renderer.domElement );

renderer.domElement.style.imageRendering = "pixelated"
renderer.domElement.style.width = (size * scale) + 'px';
renderer.domElement.style.height = (size * scale) + 'px';

var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshLambertMaterial( { color: "#433F81" } );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );

var lineMaterial = new THREE.LineBasicMaterial({ color: "#ae3F81" });

var lineGeometry = new THREE.Geometry();
lineGeometry.vertices.push(
  new THREE.Vector3( -2, 0, 0 ),
  new THREE.Vector3( 0, 2, 0 ),
  new THREE.Vector3( 2, 0, 0 ),
  new THREE.Vector3( 0, 0, 0 )
);

var line = new THREE.Line( lineGeometry, lineMaterial );
cube.add( line );

var pointMaterial = new THREE.PointsMaterial({ color: "#43ae81", size: 1, sizeAttenuation: false });

var pointGeometry = new THREE.Geometry();
pointGeometry.vertices.push(
  new THREE.Vector3( -1.5, 1.5, 2 ),
  new THREE.Vector3( 1.5, 2, -2 ),
  new THREE.Vector3( 1.5, -1.5, 0 ),
  new THREE.Vector3( 0, 0, 0 )
);

var points = new THREE.Points( pointGeometry, pointMaterial );
cube.add( points );


var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
scene.add( directionalLight );
var light = new THREE.AmbientLight( 0x404040, 5 );
scene.add( light );

/*
 * we have to change the way we're rendering the scene. instead of rendering
 * using the renderer directly, we have to use a EffectComposer so that we can
 * apply a shader pass.
 * 
 * we call loadShaderPass (defined below) which loads a palette image asynchronously
 * and constructs a ShaderPass out of it with our palette shader (also defined below).
 * this is passed into a callback which starts rendering with an EffectComposer
 * 
 * palette images need to be accessible across origins. i've uploaded a few to postimg
 * so you can try swapping them out and compare the effects they have on the rendered scene
 */
loadShaderPass(
  // "https://i.postimg.cc/qB3j1s6H/primal8-1x.png", // https://lospec.com/palette-list/primal8
  // "https://i.postimg.cc/DzRYzB45/slso8-1x.png", // https://lospec.com/palette-list/slso8
  "https://i.postimg.cc/7hSv4jBg/dynasty38-1x.png", // https://lospec.com/palette-list/dynasty38
  shaderPass => {
  /*
   * the effect composer pipeline for this scene is one where we
   * 1. render the scene normally using a RenderPass
   * 2. apply the shader constructed by loadShaderPass
   */
  let composer = new THREE.EffectComposer(renderer);
  let renderPass = new THREE.RenderPass(scene, camera)
  composer.addPass(renderPass)
  composer.addPass(shaderPass)
  /*
   * note that if you remove composer.addPass(shaderPass) the image
   * will be antialiased and blurry. you can uncomment the following line
   * to disable the antialiasing
   */
  // composer.addPass(composer.copyPass)
  
  /*
   * the render loop is similar to the earlier ones, the difference
   * being the call to composer.render()
   */
  var render = function () {
    requestAnimationFrame( render );

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    composer.render()
  };
  
  render();
})

/*
 * takes a palette as an array of THREE.Colors and returns a THREE.ShaderMaterial
 * that constrains the color of every rendered pixel to be one of the colors in
 * the palette. every pixel is replaced with the color in the palette that is closest.
 * the shader will also generate a dither pattern if two palette colors are within
 * a threshold of  ...
 * the shader is designed to work with EffectComposer.
 */
function constrainedPaletteShader(palette) {
  return new THREE.ShaderMaterial({
    uniforms: {
      /* EffectComposer compatibility, the input image */
      "tDiffuse": { value: null },
      /*
       * The palette, an array of THREE.Color. you can change the palette
       * uniform at runtime only if the size remains the same, as the size
       * gets compiled into the shader. it is usually easiest to just call
       * constrainedPaletteShader again and get a new shader if you're changing
       * the palette
       */
      "palette":   { value: palette },
      /* The threshold under which to perform a dither */
      "threshold": {value:0.03}
    },
    /* standard vert shader */
    vertexShader: [
      "out vec2 vUv;",
      "void main() {",
        "vUv = uv;",
        "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
      "}"
    ].join( "\n" ),
    fragmentShader: [
      "uniform vec3 palette[" + palette.length + "];",
      "uniform sampler2D tDiffuse;",
      "uniform float threshold;",
      "in vec2 vUv;",
      "void main() {",
        /* the input pixel */
        "vec3 color = texture2D( tDiffuse, vUv ).rgb;",
        "float total = gl_FragCoord.x + gl_FragCoord.y;",
        "bool isEven = mod(total,2.0)==0.0;",
        "float closestDistance = 1.0;",
        "vec3 closestColor = palette[0];",
        "int firstIndex = 0;",
        "int secondIndex = 0;",
        "float secondClosestDistance = 1.0;",
        "vec3 secondClosestColor = palette[1];",
        /*
         * loop through the palette colors and compute the two closest colors
         * to the input pixel color
         */
        "for(int i=0;i<" + palette.length +"; i++) {",
          "float d = distance(color, palette[i]);",
          "if(d <= closestDistance) {",
            "secondIndex = firstIndex;",
            "secondClosestDistance = closestDistance;",
            "secondClosestColor = closestColor;",
            "firstIndex = i;",
            "closestDistance = d;",
            "closestColor = palette[i];",
          "} else if (d <= secondClosestDistance) {",
            "secondIndex = i;",
            "secondClosestDistance = d;",
            "secondClosestColor = palette[i];",
          "}",
        "}",
        /* 
         * if the two closest colors are within the threshold of each other
         * preform a dither
         */
        "if(distance(closestDistance, secondClosestDistance) < threshold) {",
            "vec3 a = firstIndex < secondIndex ? closestColor : secondClosestColor;",
            "vec3 b = firstIndex < secondIndex ? secondClosestColor : closestColor;",
           "gl_FragColor = vec4(isEven ? a : b, 1.0);",
        /* otherwise use the closest color */
        "} else {",
          "gl_FragColor = vec4(closestColor, 1);",
        "}",
      "}"
    ].join( "\n" )
  });
}

/* get the image data (pixels) from an HTML image element */
function imageData(img) {
  var canvas = document.createElement('canvas');
  var context = canvas.getContext('2d');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  context.drawImage(img, 0, 0 );
  return context.getImageData(0, 0, img.naturalWidth, img.naturalHeight);
}

/* get a palette (an array of THREE.Color) from an HTML image */
function palette(img) {
  let palette = [];
  let pixels = imageData(img).data
  for (var i = 0; i < pixels.length; i+=4) {
    palette.push(new THREE.Color(pixels[i] / 256.0, pixels[i+1]  / 256.0, pixels[i+2] / 256.0))
  }
  return palette;
}

/*
 * loads the image from url asynchronously and generates a palette from it
 * the image is expected to be 1 pixel high. a the callback cb is invoked
 * passing in the generated ShaderPass when everything is done.
 */
function loadShaderPass(url, cb) {
  var image = new Image();
  image.crossOrigin = "Anonymous";
  image.src = url;
  image.addEventListener('load', function () {
    let pass = new THREE.ShaderPass(constrainedPaletteShader(palette(image)));
    cb(pass)
  })
}
<!-- we need a few more scripts out of the three.js distribution -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/ShaderPass.js"></script>
html, body {
  margin: 0;
  background: black;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}